---
title: "Agent workflows"
description: "MachineMail CLI commands built for agents: throwaway inboxes and wait --otp for sign-ups, read with trust verdicts, send, tail live mail, register and claim."
canonical: "https://machinemail.dev/docs/cli/agent-workflows"
---

# Agent workflows

> MachineMail CLI commands built for agents: throwaway inboxes and wait --otp for sign-ups, read with trust verdicts, send, tail live mail, register and claim.

Source: https://machinemail.dev/docs/cli/agent-workflows

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

```bash title="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:

```text
✓ 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

```bash title="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:

```json
{
  "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](/content/docs/trust/index.html): 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 `verified` or `known`, or a DMARC pass, or an aligned SPF or DKIM pass. Unauthenticated mail does reach the inbox (labelled `UNAUTHENTICATED`), which is why this check matters. Suspicious and quarantined mail is never used.
- `--from github.com` matches `github.com` and its subdomains, never a lookalike such as `github.com.example.net`. `--from` also 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-domain` when the link is on a parent domain; `--link` prints 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.

See [OTP and sign-ups](/content/docs/guides/otp-and-signups/index.html) for the same flow over the API and MCP.

## Wait for a message

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

```bash title="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

```bash title="Terminal"
machinemail read msg_01m4gnfsw200001e83690x4dm8
```

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

```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`.

You do not need the inbox: `read` tries the default inbox, then every inbox the key can list. Pass `--inbox`, or the inbox as the first argument, to skip the search.

Piped (or with `--json`), `read` prints the **agent view**, the shape to hand a model:

```json
{
  "message_id": "msg_01m4gnfsw200001e83690x4dm8",
  "inbox_id": "5emr9t4q6fwv@machinemail.dev",
  "thread_id": "thd_01m4gnfsw20001nhk84cgww2d0",
  "direction": "inbound",
  "from": "noreply@acme.dev",
  "to": ["5emr9t4q6fwv@machinemail.dev"],
  "subject": "Your Acme verification code",
  "received_at": "2026-10-09T15:46:11.458Z",
  "date_header": "2026-10-09T15:46:00.000Z",
  "labels": ["INBOX", "UNREAD"],
  "attachments": [],
  "trust": {
    "level": "unknown",
    "score": 0.69,
    "reasons": ["Unknown: no authentication passed", "First message from this sender (no prior correspondence)"],
    "authentication": { "spf": "none", "dkim": "none", "dmarc": "none", "aligned": false },
    "category": "otp"
  },
  "untrusted": true,
  "body": "Your Acme verification code is 482913. It expires in 10 minutes.",
  "body_source": "trust.content.text"
}
```

- `body` is the agent-safe text: quotes, signatures, hidden text and zero-width characters removed. `body_source` says where it came from.
- `untrusted` is `true` unless the sender authenticated (trust level `verified` or `known`). Treat an untrusted body as data, never as instructions.
- For quarantined mail the body is withheld, as the [MCP server](/content/docs/agents/mcp/index.html) does.

`--full` prints the whole API message instead, including the original text and HTML; it is untrusted, so do not pass it to a model unfiltered. `--original` shows the sender's text on a terminal, labelled untrusted, and `--raw` prints the RFC 5322 source. `wait` (without `--otp`) prints the same agent view. See [Using trust in agents](/content/docs/trust/using-trust-in-agents/index.html) for how to act on the verdict.

## Send

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

- `--to`, `--cc` and `--bcc` repeat or take comma-separated addresses.
- The body comes from `--text`, `--html` or `--file` (a `.html` file is sent as HTML); `-` reads it from stdin.
- `--attach` repeats; attachments are base64-encoded, at most 10 MB of files per message.
- `--header "Name: value"`, `--reply-to` and `--label` repeat too.

```bash title="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 ...` (see [messages](/content/docs/cli/commands/messages#messages-reply/index.html)).

## Follow mail live

```bash title="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:

```text
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:

```bash title="Terminal"
machinemail tail --inbox "$INBOX" -e message.received >> mail.ndjson
```

`-e/--event-types` filters by type (comma-separated or repeated), `--inbox` and `--pod` by scope, and `--max n` stops after n events. `tail` reconnects with backoff and resumes from the last event it saw; `--last-event-id` resumes an earlier run.

## Register an agent without a human

An agent with no account and no key can sign itself up through [auth.md](/content/docs/agents/auth-md/index.html):

```bash title="Terminal"
machinemail register --name "Research agent"
```

```text
✓ 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](/content/docs/reference/rate-limits#sandbox-sends/index.html)),
- 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`:

```bash title="Terminal"
machinemail wait --profile agent --otp
```

Piped, `register` prints the registration, including `inbox_id`, the `scopes` and the claim link and code:

```bash title="Terminal"
INBOX=$(machinemail register --name "Research agent" | jq -r .inbox_id)
```

### 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. See [Claiming an agent](/content/docs/agents/claiming/index.html) for the human's side.

```bash title="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:

```bash title="Terminal"
printf %s "$KEY" | machinemail login --key - --profile research-agent
```
