Agent workflows · MachineMail Docs

A handful of top-level commands cover what agents do with email most often, each in one line that works the same from a shell script or a model’s tool call. They all print JSON when piped, and the reading commands only ever hand over the agent-safe text of a message.

Job Command
Get an address machinemail inbox new
Wait for a verification code machinemail wait --otp
Wait for any message machinemail wait --from ... --subject ...
Read a message safely machinemail read <message-id>
Send machinemail send
Follow mail live machinemail tail
Start with no account machinemail register, then machinemail claim

send, wait and read use a default inbox when you leave it out: MACHINEMAIL_INBOX, then the inbox saved in the profile by register, then the inbox of an inbox-scoped key.

Sign up for a service

The most common agent job: create an address, use it to sign up somewhere, and read back the verification code.

  1. Create a throwaway inbox

Terminal

INBOX=$(machinemail inbox new --ephemeral --ttl 30m)
echo "$INBOX"   # 5emr9t4q6fwv@machinemail.dev

Without --username the address gets a random, hard-to-guess local part. --ephemeral makes the inbox delete itself, with its mail, after the TTL (one hour unless --ttl says otherwise; --ttl takes 600, 10m, 2h or 1d); each inbound message extends it. On a terminal it shows the inbox and the next command to run:

✓ Created 5emr9t4q6fwv@machinemail.dev
     ephemeral, expires 2026-10-09 16:15 UTC unless mail arrives
     Wait for a verification code sent to it:
     machinemail wait --inbox 5emr9t4q6fwv@machinemail.dev --otp
  1. Submit the sign-up form

Have the agent (or your test) enter $INBOX in the service’s sign-up form.

  1. Wait for the code

Terminal

CODE=$(machinemail wait --inbox "$INBOX" --from acme.dev --otp --timeout 2m)
echo "$CODE"   # 482913

wait blocks until a new message matching the filters arrives, then prints only the code, or the magic link when the message has no code. It exits 8 if nothing arrives within --timeout (default 5 minutes; 0 waits forever).

If the email may have arrived before wait started (you submitted the form first), add --since 5m so recent mail counts too. Only mail received after wait starts counts otherwise.

With --json you get the code and where it came from:

{
  "code": "482913",
  "value": "482913",
  "message_id": "msg_01m4gnfsw200001e83690x4dm8",
  "inbox_id": "5emr9t4q6fwv@machinemail.dev",
  "from": "noreply@acme.dev",
  "subject": "Your Acme verification code",
  "trust_level": "verified"
}

How the code is found, and what is trusted

wait --otp is built so that a hostile email cannot hand the agent a code of its choosing:

For a test sender without SPF or DKIM (a local mail server, an injected test message), add --allow-unauthenticated. It still skips suspicious and quarantined mail.

Wait for a message

Without --otp, wait prints the message itself. Filter by sender and subject (each can repeat; any match counts):

Terminal

machinemail wait --inbox "$INBOX" --from billing@acme.dev --subject invoice --timeout 10m

wait listens on the event stream and polls as a fallback (every 5 seconds while the stream is up, 2 seconds without it), so it still works when the stream drops. --poll uses polling only.

Read a message safely

Terminal

machinemail read msg_01m4gnfsw200001e83690x4dm8

On a terminal, read shows the headers, the trust verdict and the agent-safe text:

Your Acme verification code
FROM      noreply@acme.dev
TO        5emr9t4q6fwv@machinemail.dev
RECEIVED  2026-10-09 15:46 UTC
MESSAGE   msg_01m4gnfsw200001e83690x4dm8
INBOX     5emr9t4q6fwv@machinemail.dev
THREAD    thd_01m4gnfsw20001nhk84cgww2d0
LABELS    INBOX, UNREAD
TRUST     UNKNOWN  score 0.69  otp
AUTH      spf none dkim none dmarc none
SENDER    acme.dev, first contact
  ● Unknown: no authentication passed
  ● First message from this sender (no prior correspondence)
  ● Classifier urgency: Deadline or expiry wording: "expires"
  … 5 more (--json for the full report)

── AGENT-SAFE TEXT ─────────────────────────────────────────────────────────────
Your Acme verification code is 482913. It expires in 10 minutes.

This sender did not authenticate (no SPF, DKIM or DMARC pass), so the message is UNKNOWN and untrusted; wait --otp would only take a code from it with --allow-unauthenticated.

Send

Terminal

machinemail send --from "$INBOX" --to ada@example.com -s "Weekly report" --text "Numbers attached." --attach report.pdf

Terminal

echo "Build passed" | machinemail send --to ops@acme.dev -s "CI" --text -

Piped, send prints the new message_id, thread_id and timestamp. A send is never retried after a server error, so a script cannot send the same message twice. To reply in a thread, use machinemail messages reply <inbox-id> <message-id> --text ....

Follow mail live

Terminal

machinemail tail --inbox "$INBOX"

On a terminal, tail prints one line per event with the trust badge of inbound mail, under a status line:

15:46:39  message.received          UNKNOWN      5emr9t4q6fwv@machinemail.dev  noreply@acme.dev  Your Acme…
● LIVE  5emr9t4q6fwv@machinemail.dev  1 event  Ctrl-C to stop

Piped, it prints NDJSON, one {id, event, data} per line, which suits an agent reading a stream or a log file:

Terminal

machinemail tail --inbox "$INBOX" -e message.received >> mail.ndjson

Register an agent without a human

Terminal

machinemail register --name "Research agent"
✓ Registered a sandbox agent: agent-ygsa50y7nfzo@machinemail.dev
PROFILE  agent (use --profile agent)
AGENT    reg_01m4gngsfh00008vwn3sjg847x
EXPIRES  2026-10-10 15:46 UTC unless claimed
SANDBOX  this inbox only, daily send caps until claimed

CLAIM  Hand this to a human. They sign in, enter the code, and the agent joins their organization.
  https://machinemail.dev/claim?claim_attempt_token=cat_...
  code 972384  expires in 10 minutes

register creates a sandbox organization with one inbox on machinemail.dev, gets an access token, and saves everything in a profile: the current profile if it has no key, otherwise one named agent (or --profile). It never replaces a profile that holds an API key unless you pass --force. The token lasts an hour; the CLI renews it before each command, so you never handle it. Tokens are never printed.

The sandbox:

The new inbox becomes the profile’s default inbox, so send, wait and read work without --inbox:

Terminal

machinemail wait --profile agent --otp

Hand it to a human

The claim is how the agent’s human takes it over. Give them the link and the 6-digit code; they sign in to MachineMail, enter the code, and the agent and its inbox move into their organization with full access.

Terminal

machinemail claim --wait                       # a new code, then wait for the human
machinemail claim --email ada@acme.dev --wait  # only Ada can claim it

Codes expire after 10 minutes and a new one replaces the old. claim (and register --wait) with --wait blocks until the human finishes, then saves the post-claim credentials to the profile. A claim code that expires while waiting exits 8. register --email limits who can claim from the start; register --no-claim skips the first code.

A claimed agent’s token can be renewed for about a day. For an agent that keeps working longer, create an API key for it (scoped to its inbox if that is all it needs) and save it to a profile of its own:

Terminal

printf %s "$KEY" | machinemail login --key - --profile research-agent