← Back to AAI for MerusCase

Process Mail

File unprocessed PDFs to the right cases, one at a time, with code-level guards that refuse to file the wrong upload to the wrong case. For deadline-bearing documents — IMR decisions, UR denials, QME reports, DORs, hearing notices — the assistant asks what you want to do before creating any task.

The case names, claim numbers, doctors, and filenames in this page are fictitious demonstration data. They illustrate how the skill behaves. Your install operates on your real case data.

On this page

What it is

You type two words at the prompt:

aaicase> process mail

The system finds every upload in your inbox with no case attached, then processes them one at a time. For each upload:

  1. Downloads the PDF to a secure temp dir
  2. Reads it natively as vision (not text extraction)
  3. Extracts the applicant name, doc type, claim number, and document date
  4. Records that binding to disk (anti-swap evidence)
  5. Searches your caseload for a matching applicant
  6. For deadline-bearing doc types, stops and asks what strategy you want
  7. Shows a proposal: file the upload + create an activity + create a task
  8. Waits for your approval
  9. Executes the writes as one rollback-safe sequence
  10. Moves to the next upload

The whole flow is gated by code, not by prompts. The assistant cannot file the wrong upload to the wrong case even if it tries to — the binding guard refuses the write at the API layer.

Why one-at-a-time matters

The single biggest risk in mail processing is filing upload A to the case of upload B. The error mode is silent: the activity description is correct (Claude wrote a plausible summary), the filename looks reasonable, the case it lands on has the same applicant surname as the document's actual subject. Nothing in the UI screams "wrong upload."

Real failure mode this skill was designed against: a paralegal noticed two cases were tangled. Activity log on case A said "defense panel strike letter." Activity log on case B said "subpoena noncompliance notice." Both descriptions were correct. But the PDF attached to each activity belonged to the other case. The model had processed both in a batch, mixed working memory, and silently crossed the wires.

When a language model processes 10 uploads in batch, its working memory holds context from all 10 simultaneously. The model is correct about each upload individually but can confuse which upload it's writing about. One-at-a-time eliminates that failure mode at the source: only one upload, one applicant name, one proposed case is ever in context at the same time.

The skill enforces this HARD RULE: even if the attorney says "process all my mail," the assistant translates it to one-at-a-time. There's no batch flag, no way to opt out. The discipline is built into the prompt at the skill level.

The per-upload flow

For each upload, the assistant shows you a binding check first:

BINDING CHECK
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Upload ID:        [UPLOAD-ID]
  Local PDF:        /tmp/aai-mail-XXXXXX/mail_[UPLOAD-ID].pdf
  Pages read:       1, 2 (binding) + 1-8 (proposal narrative)
  Applicant (read): DOE, JANE
  Claim # (read):   ADJ[NUMBER]
  Doc date (read):  2026-05-06
  Doc type:         Subpoena Noncompliance
  Case match:       DOE, JANE #[FILE-NUM] — exact match ✓
  Filename plan:    2026-05-06-subpoena-noncompliance-doe.pdf

Then either a strategy question (for deadline-bearing docs — see below) or the proposed actions:

MAIL N of TOTAL — Upload #[UPLOAD-ID]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DOCUMENT: Subpoena Noncompliance (Second Notice)
FROM:     Sample Copy Service
ABOUT:    DOE, JANE — Case #[FILE-NUM]
KEY:      SDT non-compliance for medical records subpoena…

PROPOSED ACTIONS:
  1. ⭐ FILE upload #[UPLOAD-ID] to DOE #[FILE-NUM] as
        "2026-05-06-subpoena-noncompliance-doe.pdf"
  2. ⭐ CREATE ACTIVITY: Subpoena (type 27960) on DOE #[FILE-NUM]
  3. ⭐ CREATE TASK
        Description: Follow up on subpoena non-compliance
        Assigned:    Sample Paralegal (SP, paralegal)   ← edit: "assign to SA"
        Priority:    Normal                             ← edit: "priority high"
        Due:         2026-05-25                         ← edit: "due friday"
  S. SKIP
  D. DELETE upload
  Q. QUIT

⭐ = Recommended. Type the numbers (e.g. "1 2 3" or "all" or "skip")
Edit any task field with the phrase shown next to it (e.g. "assign to ML").

Every task line shows the proposed assignee, priority, and due date with an inline edit hint. To change a field, just say what the hint says — assign to SA, priority high, due friday. The proposal re-renders with your change applied. Nothing writes until you say yes.

Assignee picker when the role is ambiguous

Most tasks have an obvious owner — petitions go to attorneys, lien letters go to paralegals. But some tasks could reasonably go to either role ("respond to adjuster," "follow up on X"). In those cases, the assistant doesn't guess. It asks first:

Before I create this task, who should own it?

  Description: Respond to adjuster re: QME doctor swap

  Possible assignees:
    1. Sample Attorney (SA, attorney)
    2. Sample Associate (SC, attorney)
    3. Sample Paralegal (SP, paralegal)
    4. Other — type a name, initials, or role

  Which?

The list is pulled live from your firm's /users/index — there's no hard-coded staff list. New hires show up automatically after running --setup (or the assistant's auto-detection on startup).

Edit shortcuts for assignment

You can change the assignee at any point in the edit/preview loop. Recognized phrases:

Name resolution tries initials first (attorneys often type DA or ML), then first name, then last name, then full name. If nothing matches, the user list is re-shown so you can pick.

Default role routing

The assistant defaults to one of two roles based on what the task involves:

Task involves…Default role
Drafting petitions, demand letters, hearing prep, settlement decisionsAttorney
QME objections, IMR appeals, UR challenges, panel strikesAttorney
Any signature-required filing or legal advocacyAttorney
Filing lien letters, requesting wage statements, subpoena follow-upsParalegal
Appointment confirmations, calendar/scheduling, records requestsParalegal
Client communication that's purely informational (no legal advice)Paralegal
Anything else where it could go either wayAsks first

If the case has an attorney_responsible or paralegal_handling set in MerusCase, the assistant uses that specific user for the role. If multiple users have the role and the case has no specific assignment, the assistant asks.

You can approve all the starred actions with all or yes, or pick specific numbers (1 2 3). Skip with s, quit with q. After any edit, the proposal is re-shown for re-approval. Nothing writes until you say yes.

Binding extraction — what gets read from the PDF

The binding is the cryptographic anchor between an upload and a case. Before the upload can be filed to any case, a separate process (aai-bind) reads the PDF and records:

FieldWhat it means
applicant_lastThe applicant's last name as found in the document caption / RE line / signature block
applicant_firstFirst name (for disambiguation if last name is common)
applicant_sourcesWhere the name was found (page 1 caption, page 4 signature, etc.)
applicant_confidencehigh / medium / low based on how many independent sources agree
claim_numberADJ or carrier claim number
doc_dateThe document's own date (letter date, exam date) — NOT the fax/upload date
doc_typeClassified type: QME, UR Decision, IMR Decision, Subpoena, etc.
pdf_sha256SHA-256 of the PDF bytes (tamper detection)

The binding is recorded in ~/.aaicase/queue/bindings.json with mode 0o600 (owner-only) and is the basis for every misfile guard in the next section.

PDFs are read natively as vision. The Read tool gives the model the rendered pages — including scanned text, signatures, stamps, and layout. No pdf-parse, no pdftotext, no text extraction. That distinction matters because scanned documents (and most QME reports are scanned) have no extractable text but plenty of visual content. Reading natively gives the model the same view a human attorney has.

The misfile guards (the most important section)

Code-level guards in bin/merus-fetch.mjs refuse the write when the binding doesn't match the target case. Five distinct refusal modes:

1. No binding

The upload was never bound. Refuses with a suggestion to run aai-bind first.

BINDING GUARD — REFUSED
Upload [ID] has no binding.
Before filing or renaming an upload, you must run:
  node bin/aai-bind.mjs [ID]

2. Applicant mismatch

The PDF's applicant name doesn't match the target case's applicant. This is the core misfile defense.

BINDING GUARD — REFUSED (applicant mismatch)
Upload [ID] contains applicant:
  DOE, JANE
Case [CASE-ID] applicant is:
  ROE, MARIA
This write would misfile the upload. Refusing.

3. Filename format violation

The proposed filename doesn't match YYYY-MM-DD-type-...-applicantlast.pdf. Catches typos and wrong-applicant-in-filename cases.

4. Low extraction confidence

The applicant binding has fewer than 2 independent sources (caption + signature, etc.). Override available but logged.

5. Extraction explicitly failed

The binding marked applicant as UNKNOWN. Refuses by default; override available.

Each refusal:

The guards are at the merus-fetch layer. They run whether the write came from a skill, from the model directly, from a sequence step, or from a manual command. There's no bypass via "just call the API differently."

Strategy questions for IMR / UR / QME / DOR / NOH

For five document types, the assistant stops between the binding check and the proposed actions and asks you what you want to do. The reasoning: these documents carry legal deadlines AND admit multiple legitimate attorney strategies. Filing alone is rarely the right answer; auto-proposing a generic task often creates the wrong task.

Hard rule for this step: don't show recommended actions before the answer. Don't pre-mark a strategy with ⭐. Wait for the attorney's choice before building the spec.

IMR Decision

  1. Appeal the IMR (LC 4610.6(h), 30 days) → HIGH attorney task + calendar event
  2. Note the deadline only → NORMAL task
  3. No appeal — accept → activity only, no task
  4. File only → no task, no event

UR Decision

  1. Appeal via IMR application (DWC Form IMR, 30 days)
  2. Challenge UR timing — late under LC 4610(i), voidable per Sandhagen/Dubon
  3. Note the deadline only
  4. No appeal — accept
  5. File only

QME Report

  1. Object (8 CCR 31.5(a), 30 days)
  2. Request supplemental (60-day reply window per 8 CCR 38(a))
  3. Accept and proceed to rating
  4. Note the objection deadline only
  5. File only

DOR (Declaration of Readiness)

  1. Defense DOR — file response within 25 days (Rule 10417)
  2. Our DOR served — prep for hearing
  3. Note the deadline only
  4. File only

Notice of Hearing

  1. Prep for hearing — checklist task + calendar event
  2. Add to calendar only
  3. File only

Other doc types (subpoena, medical records, carrier letters, settlements, F&A awards) skip the strategy step — their default proposals are usually right.

Rollback-safe execution

Filing an upload is rarely one write. It's typically three or four:

  1. Set case_file_id + filename on the upload (/uploads/edit/[ID])
  2. Create an activity record linking the upload to the case (/activities/add)
  3. Create a follow-up task (/tasks/add)
  4. Maybe create a calendar event (/events/add) if the doc has a deadline

If write #1 succeeds but write #3 fails, you have an upload filed and an activity created but no task — a partial state that's hard to undo manually.

To prevent that, every filing runs through aai-sequence: a runner that executes the writes as one atomic-with-rollback operation. If any step fails, the runner automatically invokes undo_template on every earlier completed step in reverse order.

Example: if step 3 fails, the runner soft-deletes the activity created in step 2 and clears the case linkage from step 1. The final state matches the initial state — the upload is back in the inbox, ready to retry.

If a rollback step itself fails (rare), the run lands in "manual cleanup needed" state and is logged to ~/.aaicase/queue/sequence-runs/[ts]_[label].json. You can later run aai-undo --show [label] to see what happened and get the exact merus-fetch commands to finish the cleanup manually.

Filename convention

Every filed upload gets renamed to:

YYYY-MM-DD-doctype-details-applicantlast.pdf

Rules:

Examples:

2026-05-06-subpoena-noncompliance-doe.pdf
2026-05-12-qme-rpt-dr-sample-lumbar-roe.pdf
2026-05-15-ur-decision-epidural-denied-doe.pdf
2026-05-20-noh-msc-status-conf-roe.pdf

The filename is part of the misfile defense. The binding guard verifies that the applicant's last name appears in the filename (case-insensitive). A filename like 2026-05-06-panel-strike-doe.pdf being applied to an upload bound to ROE is refused before the API call leaves the machine.

Audit trail

Every write — accepted, rejected, or refused-by-guard — lands in ~/.aaicase/audit.log:

{"ts":"2026-05-27T17:23:00.000Z","method":"POST",
 "endpoint":"/activities/add",
 "fields":["case_file_id=[CASE-ID]","activity_type_id[]=47935","upload_id=[UPLOAD-ID]","activity=..."],
 "status":200,"outcome":"accepted"}

{"ts":"2026-05-27T17:24:30.000Z","method":"POST",
 "endpoint":"/uploads/edit/[UPLOAD-ID]","fields":["case_file_id=[CASE-ID]"],
 "status":null,"outcome":"guard_refused",
 "guard":"binding_applicant_mismatch",
 "reason":"Upload [ID] bound applicant=\"DOE\" vs case [CASE-ID] applicant=\"ROE\""}

The log is append-only, rotated by size (10 MB default), and mode 0o600. Refused writes appear alongside accepted ones so compliance review can see exactly what the system did and didn't allow.

Troubleshooting

"No unprocessed mail" but I know there's mail in the inbox

"Unprocessed" means: no case_file_id AND no activity_id on the upload record. If a previous run set case_file_id but didn't create the activity (rollback edge case), the upload is "filed but not activity-linked" — not "unprocessed." Use audit case [name] to surface these.

The skill keeps refusing to file an upload

Read the BINDING GUARD output carefully — it names exactly which check failed. Common ones:

How do I bypass a guard for one specific write?

Generate a one-time override token bound to that exact write:

node bin/aai-override.mjs applicant_mismatch upload_id=[ID] case_file_id=[CASE-ID]
AAI_OVERRIDE_TOKEN=<token> node bin/merus-fetch.mjs ...

The token is single-use, signed against the specific upload+case pair, and logged in the audit trail when redeemed. It can't be reused or applied to a different operation.

Process mail is slow on a big inbox

Most of the time is spent reading PDFs (one at a time, by design). A typical inbox of 20 uploads takes 15-30 minutes including attorney approval time. The constraint isn't speed — it's that filing the wrong upload to the wrong case is a much worse outcome than filing more slowly. The prefetcher pre-downloads the next 2-3 PDFs while you're approving the current one, which masks most of the network latency.

Part of AAI for MerusCase — code-guarded AI case intelligence for California Workers' Comp attorneys.