Rust · daemon · CLI · MCP · TUI · web · Android · iPhone · Mac

Outbox

Outbox is a review queue for the messages agents and scripts want to send. A Slack message, an email or a Teams chat waits in Outbox until you approve, edit, send back or reject it, from a browser, a Mac, an Android phone, an iPhone or Apple Watch, a terminal or the command line. Approved messages go out through a command you configure for each destination.

The Outbox web app by day: tabs for Review (4), In flight (1), Attention (2) and History; a queue of letters to Alice Moreno, mo@example.com, #incidents and #releases; and the #releases letter open on a paper sheet, from pi on the host studio in the talos repository, with a dashed 'waiting' postmark, the Markdown body, a footer reading 'sent by pi (talos-release) via Outbox', the sender's reason and link, and Approve, Edit, Changes and Reject buttons.
The web app the daemon serves: a letter from an agent, waiting for review.
outbox send slack dm --username priya --reason "She asked when the fix ships" \
    -- "The retry fix is in tonight's build; I'll confirm once it is live."
ob_kjwdj3dd  pending             0s  slack dm → Priya Shah  · claude · studio
waiting for review · outbox message wait ob_kjwdj3dd
outbox approve ob_kjwdj3dd
ob_kjwdj3dd  approved           11s  slack dm → Priya Shah  · claude · studio
sending after the undo grace · outbox undo ob_kjwdj3dd

Reviewing

Every client shows the same queue, live: Review, In flight (approved, sending, or sent back for changes), Attention (sends that failed or may not have happened) and History. Each letter shows who it is for, what will be sent, who proposed it and from where (agent, host, repository and branch), the sender's reason and links, and the body as the destination will render it.

ActionWhat happens
ApproveSends after an undo grace, 5 seconds by default; Undo takes it back.
EditSaves your version as a new revision; the sender's original is kept.
Request changesSends it back with your note; the sender revises it and it returns for review.
RejectCloses it; the sender sees your reason.
Retry, resolveFor failed sends, and for sends whose outcome is unknown.
Auto-sendPer destination or message type, for a while or until turned off: outbox auto on slack --for 2h.
The Android app's queue with a notification at the top: 'To Alice Moreno', the first line of the message, and Approve and Reject buttons.
Approve or reject from the notification.
The Android app showing the #releases letter: route from talos-release to releases, a dashed waiting postmark, the body with a bold release name and two bullet points, the footer, and buttons for Approve, Edit, Changes and Reject.
A letter on Android.
The same letter in the Android app's night theme: dark paper, light serif text and the same postmark and buttons.
The night theme.
Two home-screen widgets, by day and by night. The queue widget lists '4 waiting' next to the Outbox wordmark, a red '2 need attention' row and letters to Alice Moreno and mo@example.com with their reasons; the small widget is a dashed postmark around the number 4 with a red badge reading 2.
Home-screen widgets: the queue, and a count with a badge when sends need attention. Rows open the letter.
The web app at night on the Attention tab: a Teams digest whose send ended with exit 76, shown with a red 'unknown' postmark, the detail 'connection reset after upload', and buttons to mark it sent, mark it failed or retry.
Attention: a send that may or may not have happened waits for you to settle it.
The terminal UI: tabs with counts along the top, the queue on the left, and the #releases letter on the right with its route, postmark, body, footer, reason, link and identity, and a key strip at the bottom.
outbox on a terminal: the same queue, keyboard first.
Keys in the terminal UI and the web app
j knext, previous1–4Review, In flight, Attention, History
a uapprove, undoeedit
crequest changesxreject
rretry/search history

Lifecycle

A message's life Every message starts pending, waiting for the reviewer. Approving it moves it to approved for a five-second undo grace, then to sending while the sink's command runs. Exit 0 means sent, and the receipt is kept. Any other exit means failed; exit 75 is retried automatically with backoff, and a failed message can be retried by hand. Exit 76, a timeout, a signal or a crash means unknown: the message may or may not have gone, so Outbox never retries it on its own; the reviewer resolves it as sent or failed, or retries it. From pending, the reviewer can also request changes, which the sender answers with a revision that returns it to pending, or reject it; the sender can withdraw it, and it can expire. Those three end it. closedrejected · withdrawn · expired pendingwaiting for you approved5 s to undo sendingsink command sentreceipt kept changes requestedthe sender revises failedexit 75: retried unknownnot retried alone approve undo exit 0 exit ≠ 0 76 · timeout retry resolve request changes revise
Nothing is sent twice by accident. When a send ends in a way that does not say whether it happened (exit 76, a timeout, a signal, a crash), the message is unknown, and Outbox never retries it on its own: you mark it sent or failed, or retry it. Exit 75 means "not sent, try again" and is retried up to three times, after 30 seconds, then one and two minutes.

Sending from an agent

A sender names a destination (a sink, such as slack), a message type (dm), a target and a body. The CLI builds its flags from the daemon's catalogue, so each type's help lists its own targets and fields:

outbox send slack dm --help
outbox send slack dm --<target> VALUE [options] [-- body…]

Direct message

Target (one of):
  --username VALUE
  --user-id VALUE

Format: slack-mrkdwn · approval: review · decorated

Options:
  --body TEXT | --body-file PATH|- | -- words…   (else stdin)
  --reason TEXT            why (shown to the reviewer)
  --link URL               context link (repeatable)
  --expires 2h             expire unreviewed after
  --route NAME             return route for outcomes
  --no-route               no automatic routes
  --idempotency-key KEY    safe retries
  --from LABEL             sender label
  --wait[=decision|settled|final] [--timeout 30m]
  --field NAME=VALUE | --to KIND=VALUE
outbox message wait ob_kjwdj3dd
ob_kjwdj3dd  sent               16s  slack dm → Priya Shah  · claude · studio

MCP

outbox mcp stdio gives agents outbox_sinks, outbox_send, outbox_status, outbox_wait, outbox_revise, outbox_withdraw and outbox_list. Review tools (outbox_approve, outbox_reject, outbox_request_changes, outbox_edit, outbox_toggle) are only offered with --review-tools.

Hearing back

A sender can block with --wait, poll outbox message status, or be told: a return route delivers each decision and outcome to whoever sent the message, such as an agent's inbox. Routes and hooks run from a persisted cursor, at least once, and leave receipts (outbox receipts).

Decoration

A sink can add a footer such as sent by claude via Outbox. Agents cannot turn it off; you can, for messages you compose yourself.

CLI exit codes
0 · 1ok · failure
2invalid input
3 · 4config · auth
5not found (including an unknown target)
6conflict: the message changed meanwhile
7rejected, withdrawn or expired
8--wait timed out
9changes requested
69daemon unreachable; nothing is queued locally

Sinks are configuration

A sink type is a few lines of YAML: the target kinds it accepts, optional fields, the body format the destination wants, and the command that sends it. Outbox renders the Markdown body into that format, and runs the command on the daemon's host with a scrubbed environment.

sinks:
  outlook:
    label: Outlook
    types:
      email:
        targets: [address]
        fields:
          subject: {type: string, required: true}
          cc: {type: list}
        format: html
        command: mail-send --to "$OUTBOX_TARGET" --subject "$OUTBOX_FIELD_SUBJECT"
policy:
  rules:
    - {sink: slack, type: note, approval: auto}   # notes to yourself skip review
What the command receives
stdin, OUTBOX_BODY, OUTBOX_BODY_FILEthe rendered body
OUTBOX_TARGETthe resolved id, or the value given
OUTBOX_FIELD_<NAME>each field; all of them in OUTBOX_FIELDS_JSON
OUTBOX_MESSAGE_IDalso OUTBOX_IDEMPOTENCY_KEY, with OUTBOX_ATTEMPT
OUTBOX_SENDER_JSONwho proposed it
OUTBOX_DRY_RUN=1a test run that must not send
What its exit status means
0sent; a last stdout line {"url": …} is kept as the receipt
75not sent, retryable
76, timeout, signalunknown
anything elsenot sent

An optional resolve command checks a target when a message is proposed (alice becomes Alice Moreno, or the send fails with exit 5), suggest feeds autocomplete when composing, and check reports the sink's health.

How it fits together

Who talks to whom Agents (through the CLI or the MCP server), automations and the reviewer propose messages to one Outbox daemon, which keeps every message and event in one SQLite file. The review clients (the web app the daemon serves, the Android, iPhone and Mac apps, the terminal UI and the CLI) follow its event stream and send decisions back. When a message is approved, the daemon runs that sink's configured command, such as a Slack or Outlook sender. Return routes then deliver decisions and outcomes back to whoever sent the message, for example an agent's inbox. agentsCLI · MCP automationscron · scripts youcompose outbox daemonone SQLite fileevents · undo grace reviewweb · apps · TUI · CLI sink commandsslack · outlook · teams … return routese.g. an agent's inbox propose events · decisions approved outcome
One authority. Only the daemon changes a message, and every change is an event with a sequence number. Clients follow the event stream and act with the revision they saw, so two reviewers cannot both approve a stale copy.

Limits