Skip to main content
gtm-sdk is three layers with one-way dependencies: the CLI calls orchestration, orchestration chains adapters, and adapters wrap exactly one external service each. The layering is enforced by tach (statically, in CI) and in review — not just described here. See Module boundaries for the tach setup.

Repo layout

Layer rules

  • libs/<service>/ wraps one external SDK or API with idiomatic Python types and functions. No libs/<x> may import from libs/<y> — if two adapters need to coordinate, that belongs in src/.
  • src/ chains adapters into workflows and owns side effects. Modal @app.function and @modal.fastapi_endpoint decorators live here, never in libs/.
  • cli/ is Typer-only: parse arguments → preflight → call into src/ → render output. No business logic.
  • data-gen/ products are independent; they do not depend on each other.
Anti-patterns to reject in review: orchestration inside libs/, business logic inside cli/, cross-lib imports.

Where new code goes

  1. External SDK call? → New file in libs/<service>/. Wrap one SDK only.
  2. Multi-step flow or Modal endpoint?src/<service>/. If it defines endpoints, register the module in _ENDPOINT_MODULES in src/app.py, or its decorators never run.
  3. User-facing command?cli/<group>/ as a Typer subapp, wired into cli/main.py via app.add_typer(...).
  4. Standalone data product?data-gen/<product>/, self-contained.
  5. Webhook handler?webhooks/<name>.py, an independent Modal app (never registered in src/app.py).
Adding a new top-level package also means updating [tool.setuptools.packages.find] in the matching pyproject.toml (root, or cli/pyproject.toml for CLI packages).

The adapter pattern

Every keyed adapter exposes the same client shape: a get_client() (or module-level functions that build one) with a three-tier key resolution — explicit api_key= argument, then the api_key_scope contextvar, then the adapter’s environment variable. Secrets and API keys covers why.

Execution model

The CLI is a thin client: provider-backed commands invoke functions deployed on Modal by name rather than calling provider APIs from your machine. Local-only commands (gtm version, gmail url decode, granola export, sanity blog download, webhook list) run in-process. The CLI contract page describes the I/O and mutation-gating conventions shared by every command.
  • api/specs/ and api/samples/ are read-only reference: OpenAPI specs for external services and redacted real webhook payloads used as test fixtures. Webhook models are validated against captured payloads, not hand-authored fixtures.
  • data-gen/ is reserved for reusable, independent data products.
  • tests/ mirrors the source layout; see Development.
Last modified on August 5, 2026