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.

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:

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

mxr compose is the supported interface for a program that wants mxr to create a draft — including HTML drafts with inline images. It reports structured output under --format json/jsonl, so callers read a draft id rather than scraping a human string:

Terminal window
mxr compose --account notto --to person@example.com \
--subject "Product Digest" --html-file message.html \
--inline notto-logo=logo.png --draft --format json
{
"action": "save_draft",
"draft_id": "draft_abc123",
"account_id": "acct_...",
"subject": "Product Digest",
"to": ["person@example.com"],
"content_kind": "html",
"inline_count": 1,
"attachment_count": 0,
"local_message_id": null
}

--draft cannot send: it conflicts with --yes in the parser. Sending stays a separate mxr send DRAFT_ID call.

This CLI contract is the supported integration path for external tools such as mxr-mailmerge. The daemon socket exists but is not a public contract — prefer the CLI unless you have measured a reason not to.

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 delete DRAFT_ID one local draft
mxr drafts push DRAFT_ID one local draft + provider capability
mxr rules dry-run a rule n/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@example.com older_than:30d" --dry-run
mxr archive --account work --search "from:noreply@example.com 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@example.com older_than:30d' --account work --format json
mxr archive --search 'from:noreply@example.com older_than:30d' --account work --dry-run
mxr archive --search 'from:noreply@example.com 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).