← Back to AAI for MerusCase

Process Messages

The email companion to process-mail. AAI walks the Merus Messages inbox for the selected user, fetches the full HTML body of each message (and every other message in the same thread), reads any PDF attachments natively, files the email to a case as an Activity with the right tags pulled from the firm’s actual catalog, and writes a single approved bundle through the sequencer. One email at a time. Emails stay in the inbox.

The email addresses, applicant names, case numbers, and message IDs in this page are fictitious demonstration data. Your install runs against your real inbox.

On this page

What it is

At the prompt:

aaicase> process messages

AAI asks which user’s inbox to walk, fetches the unfiled emails (capped to the chosen user), and presents them one at a time. For each: identify the case, pick tags from the firm’s activity-type catalog, read any attachments, propose a complete filing bundle, wait for approval (or edits), execute as one rollback-safe sequence, verify against Merus, log, next.

Filing an email creates an Activity record that links the email to a case. The email itself is not moved or deleted — it stays in the Merus inbox so the attorney can still reply from the Merus UI. Filing is about putting the conversation on the case’s timeline, not removing it from the inbox.

First question: whose inbox?

Email is personal. The skill never assumes whose inbox to walk — even if a previous session picked a user, it re-asks on entry:

Which user's email inbox should I process?

  1. Sample Firm Admin (SFA) — admin@example.com
  2. Sample Attorney (SA) — attorney@aai.dev
  3. Sample Associate (SC) — associate@example.com
  4. Sample Paralegal (SP) — paralegal@example.com

Type a number, name, or initials.

The user list is pulled live from /users/index — whatever staff exists at the firm. If the original command named a person (“process the associate’s messages”), the skill skips the prompt and goes straight to that user’s inbox.

Unfiled only — emails stay in the inbox

The query for unfiled messages is precise:

The skill never touches emails that are already filed. It never deletes anything. The Merus UI is still the place to reply, forward, archive, or actually remove a message — this skill exists to do the one thing the UI is bad at: file an email to the right case with the right tags.

Body + thread — what AAI actually sees

For each message it processes, AAI fetches the full HTML body via /messages/view?message_id=<url-encoded-string-id>. Bodies can be tiny (a 50-character “ok, scheduled”) or enormous (20,000+ characters when the email carries a long forward chain). Either way, the entire body is read — Claude reads HTML natively, no extraction step.

That alone is enough to tell the case match, urgency, and what tags belong on the activity. But AAI goes further: it also fetches the entire thread — every other message that shares the same thread_uuid — so the proposal isn’t made in isolation. A “RE: 6TH FOLLOW UP Re: TTD” email read alone says nothing. The same email read in the context of 10 prior messages over six days says “the defense has been ducking us for almost a week on this TD payment.” That shapes what task gets created.

What AAI has on each message, per call:

From /messages/indexFrom /messages/view
id, message_id, thread_uuid, subject, sender_header, recipient_header_to, date_sent, upload_id, case_file_id, deleted, is_read, folder_name body (full HTML), recipient_header_cc, recipient_header_bcc, reply_to, in_reply_to, original_message_id, attachments object, s3_path (raw RFC822 source), draft_data scaffold for replies

The endpoint requires the string message_id (e.g. messages/2025-11-06/<rfc822-id@meruscase.com>) URL-encoded as a query parameter. Passing the numeric id does not work. This is the only path that exposes the body.

For attachments — if the message has any — AAI downloads each one via /documents/download/[upload_id] and reads it natively (see below). So when an email has a QME report attached, the proposal is informed by the body of the email and the full document content of the report, not by the subject line alone.

One at a time — the hard rule

If the attorney says “process all my email,” “do them all,” or “file everything” — AAI does not bulk-file. It translates to one-at-a-time and tells the attorney why.

The opener is:

“Found N unfiled emails. I’ll show them one at a time — say ‘yes’ or ‘all’ on each, and I’ll file it and move to the next. Starting with #1...”

If the attorney pushes back (“just do them all, I trust you”):

“I can’t bulk-file emails — Merus writes have no undo and one mis-matched case tags the wrong client. Per-item approval is fast: I show what I’d do, you say ‘yes’ or edit, I file it, next. Ready for #1?”

The word “all” inside the per-email flow has a different meaning: it means “approve all the proposed actions for this email.” Never “all emails.” This is the same one-at-a-time discipline that runs through process mail and process my tasks.

Tags come from the firm’s actual catalog

Every firm has its own set of Merus activity types: “Email Received,” “Email to Adjuster,” “QME/AME Reports,” “Defense Letter,” etc. The IDs differ from firm to firm. One firm’s “Email from Adjuster” might be ID 32350; another firm’s might be ID 41827.

The skill fetches the firm’s catalog at the start of every session:

node bin/merus-fetch.mjs /activityTypes/index

Then it caches the result for the session and matches by name (case-insensitive). It never hardcodes IDs. If your firm renames a tag or adds a new one, the skill picks it up the next time it runs — no aaicase update needed.

Bind by Claude — never by regex

Tag classification is enforced by a fresh claude -p subprocess. The agent loop does not pattern-match the subject line or sender address to pick tags. There is no fallback regex.

The flow is:

  1. The skill calls node bin/aai-bind-message.mjs MESSAGE_ID.
  2. That binary spawns an isolated Claude subprocess.
  3. The subprocess sees: the email metadata (sender, recipient, subject, date), the firm’s activity-type catalog, and the firm’s user directory.
  4. It picks tag IDs and emits a structured binding JSON.
  5. The binding is stored.
  6. The merus-fetch message tag guard refuses any /activities/add with a message_id if there is no binding or if proposed tags don’t match the binding’s high-certainty suggestion.

A typical binding looks like:

{
  "ok": true,
  "binding": {
    "message_id": "[ID]",
    "sender_email": "adjuster@example.com",
    "sender_role": "defense",
    "direction": "received",
    "subject": "RE: ROE v Sample Industries - QME Panel Strike",
    "suggested_activity_type_ids": [107, 32351, 47941],
    "suggested_activity_type_label": "Email Received + from Defense + QME Panel",
    "suggested_activity_type_certainty": "high",
    "case_hint_applicant_last": "ROE",
    "case_hint_claim_number": "[ADJ#]"
  }
}

The same idea as process mail’s PDF binding: classification is Claude’s job, deterministic math and IDs are JavaScript’s, and the API layer refuses writes that don’t carry a valid binding.

Attachments read natively, same as mail

If an email has attachments (the upload_id field is populated), the skill treats each one exactly like a piece of inbound mail in process mail:

  1. Download via merus-fetch --download /documents/download/[UPLOAD-ID] into a per-run mktemp -d temp dir locked to chmod 700.
  2. Plan the page-range read using lib/pdf-reader.mjs — 1–50 pp in one shot, 51–200 in sequential 50-page chunks, >200 with a TOC scan first.
  3. Read with Claude’s native vision — not pdftotext, not pdf-parse, not OCR.
  4. Apply the matching extraction template (QME, subpoena, UR denial, IMR decision, medical records, generic).
  5. Include the extracted findings — deadlines, dates, doctor names — in the proposal for the email itself.

The rule is the same as elsewhere in AAI: no proposal without evidence of read. The skill may not propose a case match, filename, activity type, task, event, or deadline unless it has actually invoked the Read tool on the downloaded attachment in this turn. Reading the email subject does not count as reading the document — many emails are forwards or auto-generated and the subject is misleading.

The proposal card — every field editable

For each email, the skill shows a summary card with proposed actions and exact values:

EMAIL 1 of 12 — Message #[ID]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

FROM:    Sample Associate <associate@example.com>
TO:      adjuster@example.com
SUBJECT: Update QME doctor — Iris Roe
DATE:    2026-05-05 09:24

ATTACHMENTS: 2026-05-09-qme-panel-letter-roe.pdf (read pp 1-3)

CASE MATCH: Roe, Iris v. Sample Industries (#[CASE-ID]) — exact match
  Reason: Applicant name in subject

DETECTED TAGS (from firm catalog):
  • Email Received        (id: 107)
  • Email to Adjuster     (id: 32351 — sender→adjuster pattern)
  • Email re QME          (id: 47935 — "QME" + panel context)

PROPOSED ACTIONS — review and edit before approving:

  1. ⭐ FILE on Roe, Iris v. Sample Industries (#[CASE-ID])
        Activity: From: Sample Associate. Subject: Update QME doctor
        Tags:     [Email Received] [Email to Adjuster] [QME/AME Reports]

  2. ⭐ CREATE TASK
        Description: Respond to adjuster: confirm new QME doctor
        Assigned:    Sample Attorney (SA, attorney)   ← edit: "assign to ML"
        Priority:    Normal                           ← edit: "priority high"
        Due:         2026-05-15                       ← edit: "due friday"

  3.   CREATE EVENT (mentioned in email)
        Title:       QME Appt — Dr. Sample
        Date/Time:   2026-06-01 10:00 AM
        Type:        QME

  S. SKIP   L. LINK to different case   Q. QUIT

The attorney can edit any field with plain English:

After every edit, the proposal is re-shown with the updated values. Nothing writes until the attorney approves the final version. Tag edits go through the message tag guard — if the attorney’s requested tag set conflicts with the binding’s high-certainty suggestion, the guard refuses the write.

Sequencer execution with rollback

Filing an email is rarely just one write. A typical bundle is: assign each PDF attachment to the case → create the email Activity → create a follow-up task → create an event. Four writes in order.

If any of those writes fails partway through, AAI does not leave the case in a half-filed state. Every bundle runs through aai-sequence.mjs with full rollback support. Each step has an args and an undo_args; if step 3 fails, the sequencer rolls back steps 1 and 2.

aai-sequence prints structured JSON to stdout:

Undo recipes use /foo/edit/{id} deleted=1 (soft delete via edit), never /foo/del/. Soft delete preserves the audit trail and bypasses the task-delete guard.

Post-write verification

After the sequence completes, the skill re-fetches every record it just wrote and confirms the writes actually stuck:

VERIFY — re-read from Merus
━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ✓ Activity #[ID] → Roe, Iris v. Sample Industries (#[CASE-ID]), tags [107, 32351, 47935]
  ✓ Message #[ID] linked to activity
  ✓ Task #[ID] — Sample Attorney, priority 2, due 05/15/2026
  ✓ Event #[ID] — QME Appt, 2026-06-01 10:00
  ✓ Attachment #[ID] → Roe, Iris (#[CASE-ID]), "2026-05-09-qme-panel-letter-roe.pdf"

If verification fails — say a tag didn’t save — the skill stops and shows expected vs. actual:

VERIFY FAILED — investigate before continuing
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ✓ Activity #[ID] created on Roe
  ✗ TAGS MISMATCH
      Expected: [107 Email Received, 32351 from Adjuster, 47935 QME/AME]
      Actually: [107 Email Received]   ← QME tag never saved

  Options:
    R — retry adding the missing tags
    E — edit and try a different tag set
    S — leave as-is, move on
    Q — quit email processing

The attorney decides. The skill never auto-retries on a verify failure.

Context-switch detection

If the attorney types a different top-level command mid-flow (“check my mail,” “morning brief,” “what is X worth,” “health check on [case]”), the skill does NOT fold that command into the current email. It stops:

Pausing email processing — 8 emails left in the queue. Run health check on Roe now and resume email later, or finish the current email first?

This avoids the worst kind of UX bug: the attorney asks “what is this case worth?” mid-email-flow and the skill treats it as an edit instruction (“case 142 worth = $50k”) or a tag change.

Edits to the current proposal are not context switches — “change task to ML,” “priority high,” “tags: just email received,” “skip,” “next,” “all” all stay within the per-email flow. When the input is ambiguous, the skill asks before guessing.

Process messages vs. process mail

process messagesprocess mail
SourceMerus Messages inboxMerus Uploads queue (scanned/uploaded PDFs)
Per-item contentSender, recipients (To/CC/BCC), subject, full HTML body, in-reply-to chain, every other message in the same thread, plus full native reads of all PDF attachmentsFull PDF content read natively
Tag bindingaai-bind-message — uses sender + subject + catalogaai-bind — uses PDF content
What it createsActivity linked to the email, optional task/event, attachments filed if presentActivity for the PDF, optional task/event, IMR/UR strategy fork when applicable
What it doesn’t doDoesn’t delete or move the email; doesn’t auto-replyDoesn’t delete the upload; doesn’t batch file
Cost per item~a small amount of API budget (no attachment), more if attachments are large~API budget scaling with PDF size

The two skills are designed to be run back-to-back: process mail first thing in the morning to file the inbound document queue, then process messages to file the email queue. The same case can pick up a Defense Letter PDF and a confirmation email about it in the same hour; both should land on the same case timeline with the right tags.

Troubleshooting

“No unfiled emails for [user]”

The inbox is clean (or the chosen user never had emails routed to them). Confirm you picked the right user; some firms route adjuster mail through a generic intake address.

“Tag refused by message tag guard”

You proposed a tag that the binding doesn’t support at high certainty. Re-run aai-bind-message on that message if the binding was stale, or use a tag that is in the binding’s suggestion. The guard exists to keep mis-tagged emails out of the case timeline.

The case match is wrong

Use the L option (Link to different case) and provide the right one. The skill re-searches and re-proposes the entire bundle against the new case. The binding’s applicant hint was wrong — usually because the email mentions multiple clients or uses initials only.

Attachment download failed

Transient API error. Skip the email (S) and re-run process messages; the queue is regenerated from scratch each time.

Verify failed and I chose “retry”

The skill retries the missing step only — not the whole bundle. If the second try also fails, it shows the same options again. There is no third auto-retry; persistent failures need attorney attention.

I want to undo a filed email

Run aai-undo. The audit log entry for the filing is keyed by label and timestamp; aai-undo reverses the writes in reverse order. The email goes back to unfiled in the inbox.

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