---
title: "Output and scripting"
description: "How the MachineMail CLI picks tables or JSON, NDJSON streams, --json, --fields and --all, errors and exit codes, CI behavior and scripting patterns with jq."
canonical: "https://machinemail.dev/docs/cli/output-and-scripting"
---

# Output and scripting

> How the MachineMail CLI picks tables or JSON, NDJSON streams, --json, --fields and --all, errors and exit codes, CI behavior and scripting patterns with jq.

Source: https://machinemail.dev/docs/cli/output-and-scripting

The CLI picks its output once per run, from where stdout goes:

| stdout                             | Output                                                                                                                                                      |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A terminal                         | Tables for lists (with a page footer), a detail pane for one object, a success line for sends and deletes. Spinners and prompts draw on stderr.             |
| A pipe or a file, or with `--json` | One JSON document per command: the API response, untouched apart from `--fields`. Streams (`tail`, `events stream`) print NDJSON, one JSON object per line. |

So the same command reads well for a person and parses cleanly for a script:

```bash title="Terminal"
machinemail messages list support-agent@machinemail.dev
```

```text
ID                              FROM               SUBJECT             TRUST    RECEIVED
────────────────────────────────────────────────────────────────────────────────────────────────────
msg_01m4gnfsw200001e83690x4dm8  noreply@acme.dev   Your Acme verific…  UNKNOWN  2026-10-09 15:46 UTC
msg_01m4gnf4m40000ar9pc867d5at  → ada@example.com  Hello               -        2026-10-09 15:45 UTC
2 messages
```

```bash title="Terminal"
machinemail messages list support-agent@machinemail.dev | jq -r '.messages[].subject'
```

```text
Your Acme verification code
Hello
```

Tables fit the terminal width: on a narrow terminal the least useful columns drop first, and ids and addresses are never cut. Dates print as UTC. Nothing is redrawn after it prints, so the output stays in your scrollback.

## Shaping JSON

**`--json`** forces JSON on a terminal too.

**`--fields a,b.c`** keeps only those fields, with dot paths for nested ones. On a list it applies to each item; on one object, to the object. In a table the fields become the columns.

```bash title="Terminal"
machinemail messages list "$INBOX" --fields message_id,from,subject
```

```json
{
  "count": 2,
  "limit": 50,
  "messages": [
    {
      "message_id": "msg_01m4gnfsw200001e83690x4dm8",
      "from": "noreply@acme.dev",
      "subject": "Your Acme verification code"
    },
    {
      "message_id": "msg_01m4gnf4m40000ar9pc867d5at",
      "from": "support-agent@machinemail.dev",
      "subject": "Hello"
    }
  ]
}
```

**`--all`** on a list command follows `next_page_token` and returns every page merged into one document, so `jq` sees one array:

```bash title="Terminal"
machinemail inboxes list --all | jq -r '.inboxes[].inbox_id'
```

Without `--all`, list commands take `--limit` and `--page-token` as the API does.

**Workflow commands print what a script needs.** `inbox new` prints only the address when piped (`--json` for the whole inbox); `wait --otp` prints only the code. Both are made for `$(...)`:

```bash title="Terminal"
INBOX=$(machinemail inbox new --ephemeral --ttl 30m)
CODE=$(machinemail wait --inbox "$INBOX" --otp)
```

## Streams: NDJSON

`machinemail tail` and `machinemail events stream` run until you stop them. Piped, they print one JSON object per line, `{id, event, data}`, flushed as each event arrives:

```bash title="Terminal"
machinemail tail --inbox "$INBOX" | jq -c '{event, from: .data.message.from, subject: .data.message.subject}'
```

```json
{"event":"message.received","from":"noreply@acme.dev","subject":"Your Acme verification code"}
{"event":"message.sent","from":"support-agent@machinemail.dev","subject":"Ping"}
```

`tail` reconnects on its own and resumes from the last event it saw. To pick up where an earlier run stopped, pass the last `id` you processed as `--last-event-id`; `--max n` stops after n events (exit 0).

## Raw and binary output

`messages raw` (the RFC 5322 source), `messages download` (an attachment), `domains zone-file` and `inboxes export` write the body as it comes. `-o/--output <file>` writes it to a file instead and prints `{path, bytes, content_type}`; `--output <dir>` keeps the server's filename. The file is written through a temporary file with mode `0600` and renamed, so it is whole or absent. A binary body is refused on a terminal unless you pass `--output` or redirect stdout.

## Input

Flags take typed values: integers and numbers are checked, booleans take `--flag`, `--flag=false` or `--no-flag`, arrays repeat the flag, and objects take JSON (`--metadata '{"team":"ops"}'`). `--ttl-seconds null` sends JSON `null` to clear a nullable field.

`-d/--data` gives the whole request body as JSON, `@file`, or `-` for stdin. Field flags override its keys:

```bash title="Terminal"
jq -n --arg to ada@example.com '{to: [$to], subject: "Report", text: "Attached."}' \
  | machinemail messages send "$INBOX" --data -
```

The CLI reads stdin only when you ask for it with `-`: `--data -`, `--text -`, `--api-key -`, `login --key -`. An agent that runs it with an open, silent stdin never hangs.

## Errors

Errors always go to stderr, so stdout carries only results. On a terminal they print as a short box with hints. When stdout is piped, with `--json`, or when stderr is not a terminal, an error is one line of JSON: the API's error envelope with the HTTP status, or a CLI error (`type` `usage` or `cli`) with `hints`.

```bash title="Terminal"
machinemail messages get "$INBOX" msg_nope
```

```text
{"error":{"type":"not_found","message":"Message not found: msg_nope"},"status":404}
```

```text
{"error":{"type":"usage","message":"Unknown action \"lst\" for messages"},"hints":["Did you mean list?"]}
```

## Exit codes

| Code | Meaning                                                                                                                               |
| ---- | ------------------------------------------------------------------------------------------------------------------------------------- |
| 0    | Success.                                                                                                                              |
| 1    | API error not listed below (400, 409, 422, 5xx after retries), an unexpected failure, or a declined confirmation.                     |
| 2    | Usage: unknown command or flag, missing argument, bad value, or a delete without `--yes` and no terminal to ask on. Nothing was sent. |
| 3    | Auth: no API key, or the API answered 401 or 403. Also a sign-in denied in the browser.                                               |
| 4    | Not found (404).                                                                                                                      |
| 5    | Rate limited (429), after retries.                                                                                                    |
| 6    | Network: the API could not be reached (DNS, connection refused, TLS, timeout).                                                        |
| 7    | The config file is unreadable or invalid.                                                                                             |
| 8    | Timed out: `wait` reached `--timeout`, or a sign-in or claim code expired.                                                            |
| 130  | Interrupted (Ctrl-C).                                                                                                                 |

Branch on them in a script:

```bash title="Terminal"
if CODE=$(machinemail wait --inbox "$INBOX" --otp --timeout 2m); then
  echo "code: $CODE"
else
  case $? in
    8) echo "no code within 2 minutes" >&2 ;;
    3) echo "check MACHINEMAIL_API_KEY" >&2 ;;
    *) echo "wait failed" >&2 ;;
  esac
  exit 1
fi
```

## Retries

Rate-limited requests (429) are retried on every method, and server errors (500, 502, 503, 504) and network failures on `GET`, `PUT` and `DELETE`, up to 3 times. The CLI honors `Retry-After`, else waits 0.5 s, 1 s, 2 s and so on with jitter (at most 8 s). A request that sends mail is never retried after a server error, so a message is not sent twice. `MACHINEMAIL_MAX_RETRIES` changes the count; `0` turns retries off.

## Deletes and confirmation

Every delete asks first on an interactive terminal: for inboxes, domains, pods, API keys and profiles you type the id, for smaller deletes (a message, a draft, a list entry) you answer y/N. Without a terminal to ask on (stdin or stderr not a terminal, or `CI` set) a delete exits 2 and sends nothing unless you pass `-y/--yes`:

```bash title="Terminal"
machinemail inboxes delete "$INBOX" --yes
```

## Color, spinners and CI

- Color follows the terminal. `NO_COLOR` (any value), `--no-color` or `TERM=dumb` turn it off; `--color` forces it on.
- Spinners appear only on an interactive stderr, and never with `CI` set or `--quiet`.
- `-q/--quiet` also drops success lines; errors still print.
- `-v/--verbose` logs each request and response to stderr with keys, webhook secrets, claim tokens and agent tokens redacted, so verbose logs are safe to paste into an issue.
- `CI` set also means no prompts: deletes need `--yes`, `login` prints the sign-in link instead of drawing the code box.

## From an agent

An agent running shell commands gets the JSON behavior automatically, because its stdout is not a terminal. A few habits keep it reliable:

- Give it a key through `MACHINEMAIL_API_KEY` (ideally scoped to its inbox) and `MACHINEMAIL_INBOX`, so `send`, `wait` and `read` need no `--from` or `--inbox`.
- Use `read` and `wait` rather than `messages get`: their JSON is the agent view, with the agent-safe text in `body` and `untrusted: true` unless the sender authenticated. See [Agent workflows](https://machinemail.dev/docs/cli/agent-workflows#read-a-message-safely).
- Check the exit code rather than parsing error text, and read `hints` from the error JSON when it is not 0.
- Pass `--yes` explicitly for deletes it is meant to do; without it a delete fails with exit 2 instead of hanging on a prompt.
- Discover commands with `machinemail <resource> --help` and `machinemail <resource> <action> --help`. Help is plain text without color when piped, and lists each command's flags, examples and the API route it calls.
