Scripting
JSON output, streams, exit codes, environment variables, and running unattended or in CI.
JSON output
With --json, stdout holds only JSON, indented by two spaces, while notes and progress stay on stderr, and nothing prompts. A list prints { items, hasMore, nextCursor }, an API object prints as the API returned it, and a hand-written command prints the object its help describes.
openemail whoami --json | jq -r .workspaceIdopenemail emails list --status failed --json | jq -r ".items[].id"An error goes to stderr as one line of JSON, and the exit code is the one a person would get:
{"error":{"type":"permission_error","code":"insufficient_scope","message":"This API key does not have the domains:write scope.","hint":"The credential is missing a scope this call needs. Use a key that has it, or sign in again with openemail login.","next":null,"status":403,"requestId":"req_7Hc2kQ","param":null,"docUrl":"https://openemail.uk/docs/api/errors#insufficient_scope","exitCode":4}}| Field | What it holds |
|---|---|
| type | The API error type, or cli_error, network_error or internal_error for a failure inside the CLI |
| code | A stable code such as insufficient_scope, not_signed_in or unknown_flag |
| message | What went wrong, in one sentence |
| hint, next | What to try, and the command to run next, or null |
| status, requestId, param, docUrl | From the API when the error came from it, otherwise null |
| exitCode | The exit code the process ends with |
Streams
Some output is a stream of JSON objects, one per line, so a pipeline can handle each item as it arrives:
- A resource list with
--allwhen stdout is not a terminal, or with--ndjson.--max <n>stops after that many items. openemail temp watch --json, one line per new message.openemail mcp serve, one JSON-RPC message per line in each direction.
openemail contacts list --all > contacts.ndjsonopenemail emails list --status failed --all --max 500 | jq -r .idExit codes
| Code | Meaning |
|---|---|
| 0 | Done |
| 1 | An unexpected failure, a server error, or a send that failed |
| 2 | A usage error: a bad argument, an unknown command or flag, a value or confirmation that could not be asked for, or an origin or path the CLI will not send a credential to |
| 3 | Not signed in, or the sign-in was refused, has expired or was signed out while the command ran |
| 4 | Not allowed: a missing scope or permission, a verification code that could not be asked for or is paused, or an API key where a browser sign-in is needed |
| 5 | Not found |
| 6 | A conflict with the current state |
| 7 | The input was invalid |
| 8 | Rate limited, or the AI allowance is spent |
| 9 | The network failed or timed out |
| 10 | Cancelled: you declined a confirmation or a prompt |
| 130, 143 | Stopped by Ctrl+C, or by SIGTERM |
Environment variables
| Variable | What it does |
|---|---|
| OPENEMAIL_API_KEY | An API key to use instead of any saved profile |
| OPENEMAIL_PROFILE | The saved profile to use |
| OPENEMAIL_BASE_URL | The API origin for OPENEMAIL_API_KEY, --api-key and commands that send no credential. A saved sign-in only ever goes to the API it signed in to |
| OPENEMAIL_APP_URL | The web app origin, for sign-in, open and docs links |
| OPENEMAIL_CONFIG_DIR | Where profiles and inbox tokens are kept, ~/.openemail unless set |
| OPENEMAIL_NO_UPDATE_CHECK | Never check npm for a newer release. OPENEMAIL_DISABLE_UPDATE_NOTICE does the same |
| NO_COLOR, FORCE_COLOR=0 | No colour |
| CI | Never prompt, never open a browser, never check for updates. Most CI services are recognised without it |
| VISUAL, EDITOR | The editor send and reply open for a body |
Unattended runs
The CLI prompts only when stdin and stdout are both terminals, and none of --json, --no-input or CI applies. Otherwise:
- A missing required value stops with exit code
2and names the flag to pass. - A destructive command stops with
Refusing to run unattended. Pass --yes to confirm.and exit code2, unless you pass--yes. - A change that needs a verification code stops with exit code
4, because nobody can type it. Use an API key, or runopenemail verifyfirst.
In CI
Give the job an API key with only the scopes it needs, keep it in a secret, and let OPENEMAIL_API_KEY carry it. Nothing is saved, nothing prompts, and no update check runs.
- name: Tell the team env: OPENEMAIL_API_KEY: ${{ secrets.OPENEMAIL_API_KEY }} run: | npx -y @openemail/[email protected] send \ --from [email protected] \ --to [email protected] \ --subject "Deployed ${{ github.sha }}" \ --text "Build ${{ github.run_number }} is live." \ --idempotency-key "deploy-${{ github.run_id }}"ADDRESS=$(npx -y @openemail/[email protected] temp new --ttl 15)./signup-test.sh "$ADDRESS"npx -y @openemail/[email protected] temp watch --first --json | jq -r .snippetnpx -y @openemail/[email protected] temp delete --yesfailed=$(openemail emails list --status failed --json | jq ".items | length")test "$failed" -eq 0Pass --idempotency-key on a send that a pipeline may retry, and derive it from what made the send necessary, such as a run id. Running the step again then returns the first send instead of mailing twice.