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.
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
Submit the sign-up form
Have the agent (or your test) enter $INBOX in the service’s sign-up form.
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:
- The code is read from
trust.content.text, the agent-safe text of the trust report: hidden text and zero-width characters are removed first, so a code planted in invisible HTML is not picked up. - Only mail whose sender authenticated is used: trust level
verifiedorknown, or a DMARC pass, or an aligned SPF or DKIM pass. Unauthenticated mail does reach the inbox (labelledUNAUTHENTICATED), which is why this check matters. Suspicious and quarantined mail is never used. --from github.commatchesgithub.comand its subdomains, never a lookalike such asgithub.com.example.net.--fromalso takes a full address.- A magic link must come from
trust.links, unflagged, and be on the sender’s domain or a subdomain of it. Pass--link-domainwhen the link is on a parent domain;--linkprints the link even when there is also a code. --pattern <regex>replaces the code pattern (the first capture group, else the whole match).
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
--to,--ccand--bccrepeat or take comma-separated addresses.- The body comes from
--text,--htmlor--file(a.htmlfile is sent as HTML);-reads it from stdin. --attachrepeats; attachments are base64-encoded, at most 10 MB of files per message.--header "Name: value",--reply-toand--labelrepeat too.
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:
- reaches only its own inbox,
- can send 10 messages to 10 different recipients per 24 hours, at most 5 per message (see Rate limits),
- expires after 24 hours unless a human claims it.
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