Skip to content

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.

  1. --format <FORMAT>table (default) for humans, json|jsonl|csv|ids for machines. The generated CLI reference lists the exact values per command.
  2. --dry-run — preview affected ids/labels/threads without mutating provider state. Implemented by core mail mutations and selected lifecycle commands.
  3. --yes — skip confirmation prompts on commands that ask before mutating. Required when stdin is not a TTY.
  4. 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.

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.

CommandReturnsPipeable formats
mxr searchenvelopes (matching messages)json, jsonl, csv, ids, table
mxr countscalar countjson, jsonl, table/text
mxr catfull message bodyjson, jsonl, table (and the --view modes for body rendering)
mxr threadthread + messagesjson, jsonl, table
mxr headersRFC 822 headersjson, jsonl, table
mxr labelslabels with countsjson, jsonl, csv, ids, table
mxr saved list / mxr saved run <name>saved searches / matchesjson, jsonl, csv, ids, table
mxr drafts listdraftsjson, jsonl, csv, ids, table
mxr replies listreply-later queuejson, jsonl, table
mxr snippets listsnippetsjson, jsonl, table
mxr storage / mxr stale / mxr response-time / mxr contacts / mxr subscriptions / mxr wrappedanalytics summariesjson, jsonl, csv, table
mxr statusdaemon healthjson, jsonl, table
mxr sync --statussync state per accountjson, jsonl, table
mxr events / mxr history / mxr logsstreaming event/log recordsjson, jsonl, csv, table
mxr notifyunread summaryjson, jsonl, text
mxr accountsruntime account inventoryjson, jsonl, csv, ids, table
mxr config showresolved configjson, jsonl, csv, ids, table
mxr config getone config valuetext
mxr attachments listattachments for a messagetable/text
mxr exportthread exportmarkdown, json, mbox, llm

Account-scoped reads:

Terminal window
mxr search "is:unread" --account work --format ids
mxr cat --search "from:alice" --account personal --first
mxr deliveries --account work --format json

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.

CommandTargets--dry-run--searchstdin IDs
mxr archivemessage(s)
mxr read-archivemessage(s)
mxr trashmessage(s)
mxr spammessage(s)
mxr star / mxr unstarmessage(s)
mxr read / mxr unreadmessage(s)
mxr label NAME / mxr unlabel NAMEmessage(s)
mxr move LABELmessage(s)
mxr snoozemessage(s)
mxr unsnoozemessage(s) or --all
mxr unsubscribemessage(s)
mxr undo MUTATION_IDone mutation
mxr send DRAFT_IDa draft✓ (--at conflicts)
mxr unsend DRAFT_IDa scheduled send
mxr drafts discarddraft(s)
mxr rules dry-runa rulen/a (always dry-run)

Account-scoped mutations use the same target set for preview and apply:

Terminal window
mxr archive --account work --search "from:noreply older_than:30d" --dry-run
mxr archive --account work --search "from:noreply older_than:30d" --yes
  • mxr (no args) — launches the TUI. There is no --format json for “the TUI.”
  • mxr daemon — is a long-running process; structured output is on mxr status / mxr events / mxr logs.
  • mxr compose / mxr reply / mxr reply-all / mxr forward — open $EDITOR by 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 --demo is legacy; use mxr demo for an isolated fake-provider profile.
  • mxr accounts add — interactive wizard by default, but goes non-interactive when you pass enough flags AND set MXR_IMAP_PASSWORD / MXR_SMTP_PASSWORD / MXR_GMAIL_CLIENT_SECRET env vars.

For agents driving mutations, follow this pattern:

1. SEARCH — mxr search '<query>' --format json
2. CONFIRM — surface the candidates to the user
3. DRY-RUN — mxr <verb> --search '<query>' --dry-run
4. APPROVE — user signs off on the diff
5. MUTATE — mxr <verb> --search '<query>' --yes
6. RECORD — capture the printed mutation_id; offer mxr undo within ~60s
7. VERIFY — mxr history --category mutation --limit 1 --format json

The 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:

Terminal window
mxr search 'from:noreply older_than:30d' --account work --format json
mxr archive --search 'from:noreply older_than:30d' --account work --dry-run
mxr archive --search 'from:noreply older_than:30d' --account work --yes
  • mxr archive / read-archive / trash / spam / star / unstar / read / unread / label / unlabel / move / snooze / unsnooze are idempotent — re-running with the same target IDs leaves state unchanged after the first call.
  • mxr send DRAFT_ID is not idempotent — calling twice will send twice (the daemon schedules the second send). Always check mxr drafts list first.
  • mxr unsubscribe may hit a provider URL once; re-running on an already-unsubscribed message is harmless but emits no useful new state.
  • mxr undo MUTATION_ID works 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 with mxr undo. A plain absent mutation_id (no undo_unavailable) just means the mutation isn’t undoable by design (e.g. star, label, move).