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.
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.
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.
The query for unfiled messages is precise:
case_file_id === null — the email hasn’t been linked to a case yet.deleted === false — not in the trash.recipient_header_to, or it’s in sender_header.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.
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/index | From /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.
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.
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.
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:
node bin/aai-bind-message.mjs MESSAGE_ID.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.
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:
merus-fetch --download /documents/download/[UPLOAD-ID] into a per-run mktemp -d temp dir locked to chmod 700.lib/pdf-reader.mjs — 1–50 pp in one shot, 51–200 in sequential 50-page chunks, >200 with a TOC scan first.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.
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.
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:
ok: true — all steps committed.ok: false + undone populated — failure cleanly rolled back. Nothing from this attempt remains in Merus.ok: false + undo_errors or irreversible non-empty — rollback was incomplete. The skill surfaces the specific steps that need manual cleanup and directs the attorney to bin/aai-undo.mjs --show <label>.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.
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.
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 | process mail | |
|---|---|---|
| Source | Merus Messages inbox | Merus Uploads queue (scanned/uploaded PDFs) |
| Per-item content | Sender, 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 attachments | Full PDF content read natively |
| Tag binding | aai-bind-message — uses sender + subject + catalog | aai-bind — uses PDF content |
| What it creates | Activity linked to the email, optional task/event, attachments filed if present | Activity for the PDF, optional task/event, IMR/UR strategy fork when applicable |
| What it doesn’t do | Doesn’t delete or move the email; doesn’t auto-reply | Doesn’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.
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.
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.
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.
Transient API error. Skip the email (S) and re-run process messages; the queue is regenerated from scratch each time.
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.
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.