Recipes
mxr is a Unix citizen. Most list/search commands support --format json|jsonl|ids|csv|table; the core mail mutations support --dry-run and accept either explicit IDs, --search QUERY, or piped IDs on stdin. The exact per-command capabilities live in the automation contract; the JSON shapes are documented in JSON output schemas. This page is the cookbook.
Each recipe shows three things:
- Situation — the actual problem you’re solving.
- Pipeline — copy-pasteable, no shell prompt prefix.
- What you get — the shape of the output.
If you’d rather hand the situation to an LLM, every section ends with a Tell an agent block — a natural-language prompt that maps cleanly onto the same pipeline.
With fzf — interactive picker
Section titled “With fzf — interactive picker”Pick a thread to read
Section titled “Pick a thread to read”Situation: too many unread messages to scroll, but you know it’s “from someone at acme”.
mxr search 'is:unread from:acme' --format jsonl \ | jq -r '"\(.message_id)\t\(.from)\t\(.subject)"' \ | fzf --delimiter='\t' --with-nth=2,3 \ | cut -f1 \ | xargs -I{} mxr cat {} --view readerWhat you get: an interactive list keyed by sender + subject; pressing Enter prints the chosen message body (rendered with reader mode) to your terminal. Use --view raw for the unrendered body, --view html to dump the original HTML.
Pick a draft to resume
Section titled “Pick a draft to resume”mxr drafts --format jsonl recover \ | jq -r '"\(.draft_id // .id)\t\(.subject // "(no subject)")"' \ | fzf --delimiter='\t' --with-nth=2 \ | cut -f1 \ | xargs mxr drafts resumeBrowse senders by volume
Section titled “Browse senders by volume”mxr storage --by sender --format jsonl \ | jq -r '"\(.bytes)\t\(.key)"' \ | sort -rn \ | fzf --header='bytes | sender' \ | awk '{print $2}' \ | xargs mxr senderWhat you get: pick the heaviest senders interactively; Enter drills into their full profile (volume, cadence, open commitments).
Tell an agent"Help me find the sender I email most often that I haven't replied to in 30 days. List the candidates first; I'll pick one."With jq — filter and reshape JSON
Section titled “With jq — filter and reshape JSON”Daily digest of unread
Section titled “Daily digest of unread”mxr search 'is:unread newer_than:1d' --format json \ | jq -r 'group_by(.from) | map({sender: .[0].from, count: length, latest: max_by(.date).subject}) | sort_by(-.count) | .[] | "\(.count)\t\(.sender)\t\(.latest)"'What you get: a per-sender digest, descending by message count, with the most recent subject for context.
Find threads that need follow-up (no reply in 7 days)
Section titled “Find threads that need follow-up (no reply in 7 days)”mxr stale --theirs --older-than-days 7 --format json \ | jq -r '.[] | "\(.thread_id)\t\(.latest_subject)\t\(.latest_date)"'--theirs means they owe me a reply (latest message is outbound).
--older-than-days 7 excludes threads with activity in the last week.
Owed-reply backlog ranked by how overdue
Section titled “Owed-reply backlog ranked by how overdue”mxr owed --format json \ | jq -r 'sort_by(-.overdue_score) | .[] | "\(.overdue_score | tostring | .[0:4])\t\(.waiting_days|round)d\t\(.counterparty_email)\t\(.subject)"' \ | head -20What you get: top 20 threads where you are the bottleneck, ranked by
waiting_days / expected_days (using the recipient’s typical cadence;
default 7 days when no history). Same set as mxr search 'is:owed-reply' — pick whichever surface fits your script.
Extract all attachment filenames from a query
Section titled “Extract all attachment filenames from a query”mxr search 'has:attachment from:billing' --format ids \ | while IFS= read -r id; do mxr attachments list "$id" doneTell an agent"Summarize my unread mail from the last 24 hours grouped by sender. Use `mxr search 'is:unread newer_than:1d' --format json` and group with jq."With xargs — bulk operations
Section titled “With xargs — bulk operations”Archive everything matching a search
Section titled “Archive everything matching a search”mxr search 'from:no-reply@*.example.com older_than:30d' --format ids \ | mxr archive --yes--yes skips the confirmation prompt; needed when stdin is not a terminal.
Trash a sender’s entire backlog
Section titled “Trash a sender’s entire backlog”mxr search 'from:spam@example.com' --format ids \ | mxr trash --yesApply a label to a query, in parallel
Section titled “Apply a label to a query, in parallel”mxr search 'from:billing@*.example.com' --format ids \ | mxr label billing --yesFor mxr-on-mxr bulk actions, prefer piping IDs directly into the mutation. Use GNU parallel only when you fan out non-mxr work.
Dry-run before committing
Section titled “Dry-run before committing”Always preview when piping into mutations:
mxr search 'from:no-reply' --format ids \ | mxr archive --dry-runWith --check — pre-send safety gate
Section titled “With --check — pre-send safety gate”Every recipe here uses the pre-send safety
pipeline. The --check flag runs every safety check WITHOUT sending,
exits 2 on any Blocker, and prints a JSON report you can parse.
Block sends with leaked secrets (pre-commit hook)
Section titled “Block sends with leaked secrets (pre-commit hook)”.git/hooks/pre-commit for a repo where you stash outgoing-mail
templates:
#!/usr/bin/env bashset -euo pipefailfor draft in mail/*.md; do to=$(yq '.to' "$draft") body=$(awk '/^---$/{n++;next} n==2' "$draft") printf '%s' "$body" | mxr compose --to "$to" --body-stdin --check --format json | \ jq -e ' ([.issues[] | select(.severity == "blocker") | .code]) as $blockers | if $blockers | length == 0 then true else error("blocked: \($blockers | join(\", \")) in \(input_filename)") end' || exit 1doneWhat you get: any commit that introduces a draft with a PEM private key, AWS/OpenAI/GitHub token, or other blocker-grade secret fails the hook before push.
Send only if safety is clean, otherwise mint and pause
Section titled “Send only if safety is clean, otherwise mint and pause”report=$(mxr send "$DRAFT_ID" --check --format json)verdict=$(echo "$report" | jq -r '.verdict')case "$verdict" in safe|warn) mxr send "$DRAFT_ID" ;; blocked) token=$(echo "$report" | jq -r '.issues[] | select(.severity == "blocker") | .override_token | select(. != null)' | head -1) echo "BLOCKED. To override: mxr send $DRAFT_ID --override-safety $token" exit 2 ;;esacAudit every scheduled send before it fires
Section titled “Audit every scheduled send before it fires”mxr drafts --format json | jq -r '.[] | select(.send_at != null) | .id' \ | while read draft_id; do verdict=$(mxr send "$draft_id" --check --format json | jq -r '.verdict') printf '%s\t%s\n' "$draft_id" "$verdict" doneWhat you get: one line per scheduled draft with its current verdict. Catch drafts that would silently fail when the scheduler fires them (the scheduler clears the schedule on Blocker, so an unaddressed warning isn’t enough — only Blockers stop a scheduled send).
Owed-reply digest emailed every morning
Section titled “Owed-reply digest emailed every morning”crontab -e:
0 8 * * 1-5 /usr/local/bin/mxr owed --format json | jq -r 'sort_by(-.overdue_score) | .[0:10] | .[] | " • \(.counterparty_email): \(.waiting_days|round)d — \(.subject)"' | { echo "Threads you owe (top 10):"; cat; } | mail -s "mxr: owed replies" you@example.comTell an agent"Walk my `mxr search 'is:owed-reply'` set. For each thread, show me atwo-line summary, then ask if I want to reply (`r`), snooze a week(`s`), or skip. Use `mxr summarize` for context. Never send withoutshowing me the body."The core mail mutations (archive, trash, spam, snooze, label, etc.) support --dry-run and print what would happen without touching the provider.
Tell an agent"Archive every unread newsletter older than 30 days. Show me the dry-run first; I'll approve."With watch — live dashboards
Section titled “With watch — live dashboards”Live unread count
Section titled “Live unread count”watch -n 30 'mxr count is:unread'Sender activity heatmap
Section titled “Sender activity heatmap”watch -n 60 'mxr search "newer_than:5m" --format json \ | jq -r ".[] | .from" | sort | uniq -c | sort -rn | head'What you get: a refreshing top-10 of senders pinging you in the last 5 minutes — useful during incidents.
Sync health monitor
Section titled “Sync health monitor”watch -n 5 'mxr sync --status --format table'Tell an agent"Tell me when @ceo emails me. Poll every 30 seconds with `mxr search 'from:ceo@example.com is:unread' --format ids` and ping me on stdout when the result is non-empty."With cron / systemd — scheduled work
Section titled “With cron / systemd — scheduled work”Morning digest at 09:00
Section titled “Morning digest at 09:00”crontab -e:
0 9 * * 1-5 /usr/local/bin/mxr search 'is:unread newer_than:1d' \ --format json | mail -s "Morning digest" you@example.comAuto-snooze low-priority newsletters until weekend
Section titled “Auto-snooze low-priority newsletters until weekend”0 17 * * 5 /usr/local/bin/mxr search 'label:newsletters is:unread' \ --format ids | /usr/local/bin/mxr snooze --until 'monday 9am' --yessystemd user timer
Section titled “systemd user timer”~/.config/systemd/user/mxr-cleanup.service:
[Service]Type=oneshotExecStart=/bin/sh -c 'mxr search "from:no-reply older_than:90d" --format ids | mxr archive --yes'~/.config/systemd/user/mxr-cleanup.timer:
[Timer]OnCalendar=Sun *-*-* 02:00:00Persistent=true
[Install]WantedBy=timers.targetsystemctl --user enable --now mxr-cleanup.timerTell an agent"Set up a weekly cron that archives no-reply mail older than 90 days. Show me the cron line; don't install it."With $EDITOR — compose loops
Section titled “With $EDITOR — compose loops”Reply to a search result interactively
Section titled “Reply to a search result interactively”mxr search 'from:alice' --format ids \ | xargs -I{} mxr cat {} --view reader \ | $EDITOR - # paste content into editor as scratchOpen a draft directly in your editor (no daemon)
Section titled “Open a draft directly in your editor (no daemon)”mxr compose already opens $EDITOR with the markdown + frontmatter shell. To prepare a one-off reply from a script:
mxr reply MESSAGE_ID --body-stdin <<'EOF'Hey — quick yes from me. Will follow up tomorrow with the deck.EOFWalk the reply queue
Section titled “Walk the reply queue”mxr replies --format ids | while read id; do mxr cat "$id" --view reader echo "Reply? [y/N/q]" read answer < /dev/tty case "$answer" in y) mxr reply "$id" ;; q) break ;; esacdoneWith grep / ripgrep — content search across exports
Section titled “With grep / ripgrep — content search across exports”# Export everything from a sender, search the corpus locallymxr search 'from:legal@example.com' --format ids \ | while IFS= read -r id; do mxr export "$id" --format markdown; done \ | rg -i 'NDA|confidential|terms'# Fast full-text grep over message bodies via Tantivymxr search 'NDA OR "non-disclosure"' --format jsonl \ | jq -r '.subject + " — " + .from'The second form is ~100× faster because it stays inside the search index. Use the first only when you need regex or ripgrep features the search grammar doesn’t support.
With parallel — fan-out work
Section titled “With parallel — fan-out work”Fetch bodies for many threads concurrently
Section titled “Fetch bodies for many threads concurrently”mxr search 'from:billing' --format ids \ | parallel -j8 mxr cat {} --view reader \ | rg -i 'amount due|invoice' \ | headPer-account sync in parallel
Section titled “Per-account sync in parallel”mxr accounts --format ids \ | parallel -j4 mxr sync --account {}Talking to your agent
Section titled “Talking to your agent”mxr is designed so agents can run it directly. Three rules keep the interaction safe:
- Read-only first. Have the agent run
mxr search,mxr cat,mxr stale,mxr storage --by sender,mxr sender,mxr summarizebefore any mutation. - Always
--dry-runbefore bulk mutations. Every mutation supports it; the agent should preview the affected IDs and report them back to you. - Use
--format jsonor--format jsonlwhen piping into the agent’s reasoning loop, nevertable(that’s for humans).
Prompt patterns that work
Section titled “Prompt patterns that work”- “Show me the 10 senders I owe replies to. Use
mxr staleandmxr senderto verify cadence.” - “Find every newsletter from this month, group by sender, and propose a label rule for the noisiest three.”
- “Draft a polite decline to the latest message from acme.com, but show me the draft before sending.”
- “Summarize unread mail from the last 24h grouped by importance. Use
mxr summarizeonly on threads with 4+ messages.”
Read-only fast paths agents should know
Section titled “Read-only fast paths agents should know”mxr search '<query>' --format json # full searchmxr cat MESSAGE_ID --view reader # rendered, distraction-freemxr summarize THREAD_ID # LLM Markdown summary + next stepsmxr sender alice@example.com # per-sender aggregatesmxr stale --mine --older-than-days 7 --format jsonmxr count <query> # count without payloadmxr drafts --format json # what's waitingmxr doctor --format json # daemon healthMutating fast paths to gate behind review
Section titled “Mutating fast paths to gate behind review”mxr archive ID --dry-run # previewmxr archive --search 'from:noreply older_than:30d' --dry-runmxr archive --search 'from:noreply older_than:30d' --yesmxr label <name> ID --dry-runmxr snooze ID --until '...' --dry-runmxr send DRAFT_ID --dry-run # preview sendmxr screener allow|deny|feed|paper-trail <addr>The agent doesn’t need a special API. The same flags humans use are the same ones it composes.
With AI features — synthesis with citations
Section titled “With AI features — synthesis with citations””What did we decide about X last quarter?”
Section titled “”What did we decide about X last quarter?””Situation: you need a grounded answer plus the messages that prove it.
mxr ask "what did Alice and I decide about pricing in Q2?" \ --from alice@example.com \ --after 2026-04-01 --before 2026-06-30 \ --format json \ | tee /tmp/answer.json \ | jq -r '.text, "\nCitations:", (.citations[] | "- \(.message_id)\t\(.subject)")'What you get: the synthesized answer to stdout, JSON to /tmp/answer.json. jq prints the answer text followed by citation rows you can pipe back into mxr cat. If retrieval can’t support an answer the text is literally “not enough evidence” — no synthesized confidence.
Resolve overdue commitments before standup
Section titled “Resolve overdue commitments before standup”Situation: it’s Friday and you want to know what you promised to send this week.
mxr commitments --status open --format json \ | jq -r '.[] | select(.direction == "yours" and .by_when != null) | "\(.by_when)\t\(.contact_email)\t\(.what)\t\(.evidence_msg_id)"' \ | sort \ | column -t -s $'\t'What you get: a sortable table of every open promise with the source message id — pipe --format ids and xargs -I{} mxr cat {} --view reader if you want to read the original draft text.
Find an expert before forwarding
Section titled “Find an expert before forwarding”Situation: inbound question you’d otherwise forward — find who’s answered something similar.
mxr expert MESSAGE_ID --format json \ | jq -r '.[0:3] | .[] | "\(.score | tostring | .[0:4])\t\(.email)\t\(.reason)"'What you get: top 3 candidate experts with score and the cited reason. Their citations[] point at the answer messages, not at the matching questions — verify before forwarding.
Re-enter a dormant thread
Section titled “Re-enter a dormant thread”Situation: you’re about to reply on a 3-month-old thread.
mxr briefing thread THREAD_ID --format json \ | jq -r '.body_markdown, "\nCitations:", (.citations[]? | " - \(.message_id // .thread_id): \(.quote)")'What you get: a Markdown recap plus citations when the model grounded claims in specific messages. Cached, so the second run is instant.
Pick a send slot that matches the recipient
Section titled “Pick a send slot that matches the recipient”Situation: scheduling a sensitive ask, want to land it in their fastest reply bucket.
mxr send-time alice@example.com --at "$PROPOSED_AT" --format json \ | jq -r 'if .confidence == "low" then "low confidence — no recommendation" else "best window: \(.recipient_rows[0].best_windows[0] | "\(.weekday) \(.hour_start):00-\(.hour_end):00")" end'What you get: a one-liner with the best window if mxr has enough data, or an honest “no recommendation” line when sample count is low. Pair with mxr send DRAFT_ID --at "$WHEN" to actually schedule.
Tell an agent"Before scheduling DRAFT_ID with `mxr send DRAFT_ID --at <when>`, run`mxr send-time <to_address> --at <when> --format json`. If the proposedslot is at least 2x slower than the best window AND confidence is mediumor high, propose the better slot. Don't auto-reschedule."See also
Section titled “See also”- CLI reference — every command and flag.
- Automation contract — which commands support
--format json,--dry-run, stdin IDs. - JSON output schemas — canonical field names for piping into
jq. - HTTP bridge — same surface over HTTP for web, mobile, and agent clients.
- API explorer — interactive Scalar reference; try requests against your local daemon.
- For agents — boundaries and safe defaults when an LLM is driving.
- AI agent skill — install the mxr skill into Claude / Cursor / Continue.
- Forgotten work — commitments and owed-reply lens behind the recipes above.
- Archive intelligence — citation-validated
mxr askand the decision log. - Briefings and loop-in — dormant-thread briefings, expert finder, suggest-recipients, whois.
- Timing and cadence — send-time optimizer and cadence watchlist.