Automation contract
mxr is built to be scripted, but not every command supports every automation primitive. This page is the contract — the things you can rely on when piping mxr into a shell pipeline or an LLM agent.
When in doubt, the auto-generated CLI reference is the source of truth. The tables below summarise the patterns.
The four primitives
Section titled “The four primitives”--format <FORMAT>—table(default) for humans,json|jsonl|csv|idsfor machines. The generated CLI reference lists the exact values per command.--dry-run— preview affected ids/labels/threads without mutating provider state. Implemented by core mail mutations and selected lifecycle commands.--yes— skip confirmation prompts on commands that ask before mutating. Required when stdin is not a TTY.- stdin IDs — pass message IDs on stdin, one per line. Equivalent to listing them as positional args. Available on most mutations; not all.
Most mail-facing commands also accept --account <selector>. Use it
to restrict a read, search, list, draft, delivery, invite, saved-search
run, or mutation to one enabled account. Selectors accept account key,
email address, account id, or an unambiguous display name.
When omitted, commands keep their normal behavior. Search, list, read, and batch mutation surfaces operate across all enabled accounts by default. Unknown or ambiguous selectors fail before mxr sends the daemon request, and direct-ID commands validate that the selected account owns the target before acting.
Reads (commands that return data)
Section titled “Reads (commands that return data)”These are the common automation-oriented read surfaces. Exact formats live in the generated CLI reference. JSON shapes per command live in JSON output schemas.
| Command | Returns | Pipeable formats |
|---|---|---|
mxr search | envelopes (matching messages) | json, jsonl, csv, ids, table |
mxr count | scalar count | json, jsonl, table/text |
mxr cat | full message body | json, jsonl, table (and the --view modes for body rendering) |
mxr thread | thread + messages | json, jsonl, table |
mxr headers | RFC 822 headers | json, jsonl, table |
mxr labels | labels with counts | json, jsonl, csv, ids, table |
mxr saved list / mxr saved run <name> | saved searches / matches | json, jsonl, csv, ids, table |
mxr drafts list | drafts | json, jsonl, csv, ids, table |
mxr replies list | reply-later queue | json, jsonl, table |
mxr snippets list | snippets | json, jsonl, table |
mxr storage / mxr stale / mxr response-time / mxr contacts / mxr subscriptions / mxr wrapped | analytics summaries | json, jsonl, csv, table |
mxr status | daemon health | json, jsonl, table |
mxr sync --status | sync state per account | json, jsonl, table |
mxr events / mxr history / mxr logs | streaming event/log records | json, jsonl, csv, table |
mxr notify | unread summary | json, jsonl, text |
mxr accounts | runtime account inventory | json, jsonl, csv, ids, table |
mxr config show | resolved config | json, jsonl, csv, ids, table |
mxr config get | one config value | text |
mxr attachments list | attachments for a message | table/text |
mxr export | thread export | markdown, json, mbox, llm |
Account-scoped reads:
mxr search "is:unread" --account work --format idsmxr cat --search "from:alice" --account personal --firstmxr deliveries --account work --format jsonMutations (destructive or stateful)
Section titled “Mutations (destructive or stateful)”Core mail mutations accept either explicit message IDs as positional args, --search QUERY for batch ops, or piped IDs on stdin. Use the generated CLI reference for non-mail lifecycle commands.
| Command | Targets | --dry-run | --search | stdin IDs |
|---|---|---|---|---|
mxr archive | message(s) | ✓ | ✓ | ✓ |
mxr read-archive | message(s) | ✓ | ✓ | ✓ |
mxr trash | message(s) | ✓ | ✓ | ✓ |
mxr spam | message(s) | ✓ | ✓ | ✓ |
mxr star / mxr unstar | message(s) | ✓ | ✓ | ✓ |
mxr read / mxr unread | message(s) | ✓ | ✓ | ✓ |
mxr label NAME / mxr unlabel NAME | message(s) | ✓ | ✓ | ✓ |
mxr move LABEL | message(s) | ✓ | ✓ | ✓ |
mxr snooze | message(s) | ✓ | ✓ | ✓ |
mxr unsnooze | message(s) or --all | ✓ | — | ✓ |
mxr unsubscribe | message(s) | ✓ | ✓ | ✓ |
mxr undo MUTATION_ID | one mutation | ✓ | — | — |
mxr send DRAFT_ID | a draft | ✓ (--at conflicts) | — | — |
mxr unsend DRAFT_ID | a scheduled send | — | — | — |
mxr drafts discard | draft(s) | — | — | — |
mxr rules dry-run | a rule | n/a (always dry-run) | — | — |
Account-scoped mutations use the same target set for preview and apply:
mxr archive --account work --search "from:noreply older_than:30d" --dry-runmxr archive --account work --search "from:noreply older_than:30d" --yesWhat’s not scriptable
Section titled “What’s not scriptable”mxr(no args) — launches the TUI. There is no--format jsonfor “the TUI.”mxr daemon— is a long-running process; structured output is onmxr status/mxr events/mxr logs.mxr compose/mxr reply/mxr reply-all/mxr forward— open$EDITORby default. For scripts, use--body,--body-stdin,--yes, and--dry-run, or use the HTTP bridge’s compose endpoints.mxr setup— interactive first-run account setup.mxr setup --demois legacy; usemxr demofor an isolated fake-provider profile.mxr accounts add— interactive wizard by default, but goes non-interactive when you pass enough flags AND setMXR_IMAP_PASSWORD/MXR_SMTP_PASSWORD/MXR_GMAIL_CLIENT_SECRETenv vars.
Safe agent loop
Section titled “Safe agent loop”For agents driving mutations, follow this pattern:
1. SEARCH — mxr search '<query>' --format json2. CONFIRM — surface the candidates to the user3. DRY-RUN — mxr <verb> --search '<query>' --dry-run4. APPROVE — user signs off on the diff5. MUTATE — mxr <verb> --search '<query>' --yes6. RECORD — capture the printed mutation_id; offer mxr undo within ~60s7. VERIFY — mxr history --category mutation --limit 1 --format jsonThe loop is the same whether the agent is claude, cursor, aider, or a hand-rolled curl-and-jq script. The contract above guarantees every step is composable.
When the user names an account, keep that selector on every step:
mxr search 'from:noreply older_than:30d' --account work --format jsonmxr archive --search 'from:noreply older_than:30d' --account work --dry-runmxr archive --search 'from:noreply older_than:30d' --account work --yesIdempotency
Section titled “Idempotency”mxr archive/read-archive/trash/spam/star/unstar/read/unread/label/unlabel/move/snooze/unsnoozeare idempotent — re-running with the same target IDs leaves state unchanged after the first call.mxr send DRAFT_IDis not idempotent — calling twice will send twice (the daemon schedules the second send). Always checkmxr drafts listfirst.mxr unsubscribemay hit a provider URL once; re-running on an already-unsubscribed message is harmless but emits no useful new state.mxr undo MUTATION_IDworks within a 60-second window; after that it returns an error.- An undoable mutation normally returns a
mutation_id. If that field is absent and the result has"undo_unavailable": true, the mutation succeeded but its undo entry could not be recorded — it can’t be reversed withmxr undo. A plain absentmutation_id(noundo_unavailable) just means the mutation isn’t undoable by design (e.g. star, label, move).
See also
Section titled “See also”- CLI reference — every command, every flag, auto-generated
- JSON output schemas — field names you can pipe into
jq - Recipes — copy-pasteable pipelines using these primitives
- For agents — patterns and safe defaults when an LLM is driving