CLI for agents
The bird CLI's primary caller is an agent, not a human. Every command honors the same contract — machine-readable output by default, errors as structured data on stderr, exit codes you can branch on — so a shell-capable agent can discover, construct, verify, and run any Bird operation without scraping help text or parsing prose. If you haven't installed the CLI yet, the CLI quickstart gets you from zero to a sent email; this page is the contract underneath it.
Exit codes first
Branch on the exit code, never on error text. The codes are semantic and stable:
| Code | Meaning |
|---|---|
| 0 | Success. |
| 1 | Everything else — unexpected or unrecognized failure. Surface and stop. |
| 2 | Invalid usage or input — bad flags, arguments, or request body. |
| 3 | Resource not found. |
| 4 | Auth or permission denied. |
| 5 | Conflict or failed precondition. |
| 6 | Transient — rate limit or server error; safe to retry, honoring retry_after from the error envelope. |
A caller that handles 2, 3, 4, and falls through to 1 covers most loops; 5 and 6 matter once you retry or mutate concurrently. Input validation fails fast: a bad enum value like --status nope exits 2 locally, before any request is made.
Output contract
- JSON to stdout by default. Data is machine-readable with no flag needed. There is no --format json to remember — it's the default; the flag exists (-f/--format) so humans can opt into text.
- --format text is for single-record commands only — get, show, status render a human card. list commands always emit JSON, so bird email list | jq -r '.data[].id' always works.
- Lists are bounded and cursor-paged. Every list returns a {data, next_cursor, …} envelope with a default --limit; page with --starting-after and narrow with per-resource filters (--status, --to, --from, …).
- Data goes to stdout, diagnostics and errors to stderr — never mixed, so piping stdout to jq is always safe.
- Errors are a JSON envelope on stderr. The body is {"error": {…}} with machine fields to branch on — exit_code, code (stable ID), type, retryable and retry_after, param and details for the offending input, and next_actions listing runnable bird commands to recover. message, request_id, and doc_url are for humans and escalation — never parse them.
Step 0: confirm auth
Before doing anything else in a loop, confirm the CLI is authenticated and the token actually works:
Przykład kodu
bird auth statusauth status always exits 0 — the verdict is the valid field in its JSON output, not the exit code. Gate on valid == true: the field is false when the API rejected the token and omitted entirely when no token is configured or the check was skipped (--offline), so jq -e '.valid == true' is the correct test. The output also reports the workspace, organization, region, granted scopes, and token expiry, plus any credentials-file parse error that would otherwise fail silently.
Auth model
Auth is OAuth-only — there is no static API key path. The --api-key flag and BIRD_API_KEY variable were removed from the CLI; bird auth login runs a browser PKCE flow (or a device flow with --device on headless machines) and stores a workspace-scoped OAuth grant that refreshes automatically on use. auth login is the one command that blocks on human approval, so run it ahead of time, not inside the loop.
Two consequences for agents:
- One token, one workspace, one region. The granted token is bound to the workspace chosen at consent, and its region is baked into the token prefix (bt_{region}_…). The CLI derives the API base URL from that region — there is no host to configure. To act in a different workspace, a human re-runs bird auth login.
- Override the endpoint only when you mean it. --base-url (or BIRD_API_URL) overrides the region-derived host, for testing against non-production environments. bird config show prints the resolved base URL and which source it came from.
Self-discovery
An agent shouldn't need this page memorized — the CLI describes itself:
- bird commands prints the entire command tree as JSON, including the machine-readable error contract — one call enumerates the whole surface.
- bird config show prints the resolved configuration (base URL, file paths) and where each value came from.
- --example on any write command prints a complete, valid request body and exits — generated from the API schema, needs no credentials.
- --dry-run on any write command prints the resolved request body that would be sent and exits without sending — the verification gate before an irreversible action.
- --idempotency-key <key> makes a retry safe: the same key replays the original result instead of acting twice.
- Destructive commands (delete, remove) require an explicit ID and --yes, so a retried loop can't quietly destroy state.
Write commands take input three ways — flags, a JSON body on stdin, or both, with the inline flag winning — so one piped template serves many calls, and unknown JSON fields are rejected with exit 2 rather than silently sent. The full loop: bird commands (what exists) → --example / --help (the shape) → --dry-run (what it'll do) → run, with --idempotency-key for safe retries.
A worked agent loop
Check auth, send, branch on the exit code, extract the ID, poll until delivered:
Przykład kodu
#!/usr/bin/env bash
set -u
# Step 0: token configured AND valid? (auth status always exits 0 — branch on the field)
if ! bird auth status | jq -e '.valid == true' > /dev/null; then
echo "not authenticated — run: bird auth login" >&2
exit 1
fi
# Send, capturing data (stdout) and the error envelope (stderr) separately.
result=$(bird email send \
--from onboarding@messagebird.dev \
--to delivered@messagebird.dev \
--subject "Agent loop test" \
--text "Sent by an agent." 2> err.json)
code=$?
# Branch on the exit code, not on prose.
case $code in
0) ;;
2) echo "invalid input: $(jq -r '.error.param // "?"' err.json)" >&2; exit 2 ;;
4) echo "permission denied — re-run: bird auth login" >&2; exit 4 ;;
*) jq '.error' err.json >&2; exit "$code" ;;
esac
# Extract the ID from the JSON on stdout, then poll the status.
id=$(jq -r '.id' <<< "$result")
for _ in $(seq 1 10); do
status=$(bird email get "$id" | jq -r '.status')
echo "status: $status"
[ "$status" = "delivered" ] && exit 0
sleep 3
done
echo "not delivered yet — check later: bird email get $id" >&2
exit 1Every branch reads structure: the valid field, the exit code, .error.param, .id, .status. Nothing parses a human sentence.
Go deeper
- CLI reference — every command, flag, and output shape.
- CLI quickstart — install, log in, and send your first email from the terminal.
- MCP server — the same Bird surface as MCP tools, for agents that call tools instead of running shells. The hosted server (mcp.platform.bird.com) needs no CLI at all; the CLI is what hosts the local stdio option (bird mcp).
- Agent skills — packaged procedures that teach coding agents Bird workflows on top of the CLI.