Skip to main content
Every gtm command follows the same contract. Learn it once and every command group — accounts, apollo, attio, exa, parallel, and the rest — behaves predictably, whether a person or an agent is driving.

JSON on stdout, everything else on stderr

Success output is JSON printed to stdout. Progress, warnings, and errors go to stderr. That makes every command safe to pipe:
uv run gtm attio people search --email-domain "acme.com" | jq '.[].email'
Runtime errors print to stderr prefixed with Error: and exit 1. Usage errors exit 2 (and emit a cli.usage_error telemetry event with the raw arguments).
Exit codeMeaning
0Success — stdout holds the JSON result.
1Runtime error — message on stderr, stdout empty.
2Usage error — bad flags or arguments.

Mutation safety is command-specific

Commands that write to external systems expose the safety control appropriate to their workflow. Commands with preview/apply behavior print the planned mutation and write nothing until --apply is supplied:
# Preview: prints what would be written to Attio, writes nothing.
uv run gtm accounts batch-add-people \
  --records '[{"email": "jane@acme.dev", "first_name": "Jane", "last_name": "Doe"}]'

# Execute: same command plus --apply.
uv run gtm accounts batch-add-people \
  --records '[{"email": "jane@acme.dev", "first_name": "Jane", "last_name": "Doe"}]' \
  --apply
The people mutation commands are different: attio people add, update, and upsert write when invoked. Their --modal-sync check|deploy|skip option controls only the Modal deployment-parity preflight before the write; it is not a preview/apply switch. The preview/apply controls are:
  • --apply — on accounts batch-add-people, accounts batch-add-companies, attio companies create-attribute-type, and attio enrichment backfill-domains. Default is a preview that performs no writes.
  • --modal-sync check|deploy|skip — on attio people mutations (add, update, upsert). This governs the Modal deployment-parity preflight before the mutation runs: check (default) verifies the deployed app matches your local code, deploy refreshes it first, and skip bypasses the preflight. These commands also accept --no-connectivity-probe to skip the Modal reachability check; neither option prevents the Attio write.
Review the command’s mutation behavior before running it. In particular, people mutations write immediately; use upsert when the intended behavior is create-or-update.

Remote execution on Modal

Provider-backed commands do not call provider APIs from your machine. The CLI validates your input, then invokes a function on your deployed Modal app by name (modal.Function.from_name(MODAL_APP, ...)) and prints the JSON it returns. The app name comes from the MODAL_APP environment variable, defaulting to gtm-sdk. This means keyed commands need a deployed app (uv run modal deploy deploy.py) and Modal credentials — see the quickstart and Secrets and API keys. Local-only commands (version, hello, gmail url decode, granola export, sanity blog download, webhook list) work with no deployment at all.

The --json override

Every command that builds a request from flags also accepts --json with a complete payload, which overrides the flags. Useful when an agent already holds a structured request:
uv run gtm exa search --json '{"query": "developer-first CRM tools", "num_results": 5}'
Payloads are validated against the same Pydantic models the SDK uses; validation failures print the model error to stderr and exit 1.

API-key override flags

Commands that call a provider accept a per-invocation key override (--exa-api-key, --attio-api-key, --apollo-api-key, --parallel-api-key, …). The value is forwarded to the deployed function and applied only for that call — the resolution order is documented in Secrets and API keys.
Last modified on July 14, 2026