Go-To-Market SDK + CLI for account research, enrichment, CRM sync, and outreach. Layered architecture: thin CLI → workflow orchestration → single-SDK adapters. Deployable as Modal serverless functions; consumable as an editable Python package or git submodule.
- Package name:
gtm(entrypoint script:gtm = cli.main:run) - Python:
>=3.13,<3.14 - Package manager:
uvonly (never barepip) - License: MIT
gtm-sdk/
├── cli/ # Thin command surface (Typer). Parses flags, preflight, calls src/.
├── src/ # Workflow orchestration. Chains libs/ adapters. Modal endpoints register here.
├── libs/ # Single-SDK adapters. One folder per external service. NO cross-lib imports.
├── data-gen/ # Reusable data generation/enrichment pipelines (independent, composable).
├── webhooks/ # Standalone Modal webhook handlers (e.g. GCP raw/ETL exporters).
├── api/
│ ├── specs/ # External API OpenAPI specs (e.g. caldotcom)
│ └── samples/ # Sample payloads (rb2b, caldotcom)
├── tests/ # pytest, importlib mode. Mirrors src/, libs/, cli/.
├── tmp/ # Gitignored scratch. ALL temporary files go here.
├── worktrees/ # Gitignored. All git worktrees under this dir.
├── deploy.py # Modal deploy entrypoint (must stay at root — avoids `attio` pkg shadowing).
├── pyproject.toml
└── uv.locklibs/<x>/wraps one external SDK or API. Idiomatic Python types/functions. Nolibs/<x>may import fromlibs/<y>.src/chains adapters into workflows. Modal@app.function/@modal.fastapi_endpointdecorators live here.cli/is Typer-only: parse args → preflight → call intosrc/→ render. No business logic.data-gen/products are independent; do not depend on each other.
Anti-patterns: orchestration inside libs/; business logic inside cli/; cross-lib imports.
| Adapter | Purpose |
|---|---|
apollo |
People + organization enrichment (via gtm-apollo) |
attio |
CRM: companies, people, notes, attributes, values |
browserbase |
Headless browser sessions |
caldotcom |
Cal.com bookings/events |
dlt |
dltHub filesystem destinations (GCP + local), DestinationType |
fathom |
Meeting recordings + transcripts |
filesystem |
File utilities for pipeline I/O |
gmail |
Gmail URL decoding |
granola |
Local Granola export reader |
harvest |
LinkedIn lead/profile data via Harvest |
linkedin |
LinkedIn member-data helpers |
octolens |
Mention monitoring |
openai |
Lead extraction via OpenAI |
parallel |
Parallel.ai web search / extract / findall |
parsers |
Generic parsers |
perplexity |
Perplexity API |
rb2b |
RB2B website visitor identification |
resend |
System/transactional email |
telemetry.py |
OTEL tracer init + emit_cli_event |
src/app.py— ModalAppdefinition, image build, secret bindings (apollo,attio,parallel), endpoint module registration. Edit here when adding new Modal endpoints.src/modal_app.py—MODAL_APPname (env-overridable viaMODAL_APP, defaultelvis-ai-v2).src/api_keys.py— API key resolution.src/enrichment.py— Enrichment workflow.src/accounts/—accounts,research,people,batch,tasks,models.src/attio/—companies,people,notes,deployment_parity,http_responses.src/apollo/—organizations,people.src/parallel/—extract,findall,search.src/{caldotcom,fathom,granola,octolens,rb2b}/— workflow modules per integration.
Run via uv run gtm <group> <command> (or uv run python -m cli.main).
gtm
├── hello, version
├── accounts GTM workflow commands
│ ├── research Non-mutating research
│ ├── enrich Non-mutating enrichment
│ ├── find-people Non-mutating people discovery
│ ├── map-account-hierarchy Non-mutating hierarchy mapping
│ ├── batch-add-people Batch add (preview/apply)
│ └── batch-add-companies Batch add (preview/apply)
├── apollo
│ ├── people People enrichment + search
│ └── organizations Org enrichment + search
├── attio
│ ├── people Manage people records
│ ├── companies Manage company records
│ └── notes Manage notes
├── enrichment
│ └── enrich Enrich records from LinkedIn (Harvest)
├── gmail
│ └── url Gmail URL decoding
├── granola
│ └── export Local Granola export
└── parallel
├── extract Extract content from URLs
├── findall Discover entities (FindAll)
└── search Search the webCLI helpers: cli/json_encoder.py, cli/json_validation.py. CLI emits OTEL events (cli.usage_error on exit code 2).
git submodule add git@github.com:elviskahoro/gtm.git gtm-sdkIn the parent pyproject.toml:
[tool.uv.sources]
gtm = { path = "gtm-sdk", editable = true }Then uv sync. All cli, src, libs packages become importable.
git clone --recurse-submodules git@github.com:elviskahoro/gtm.git
cd gtm
uv syncuv sync # install/lock deps
uv run gtm --help # CLI help
uv run gtm <group> <cmd> --help # subcommand help
uv run pytest # full test suite (importlib mode)
uv run pytest tests/cli # subset
trunk check --all # lint + typecheck (ruff, etc.)uv run modal deploy deploy.pydeploy.pylives at the repo root on purpose — moving it undersrc/causessrc/attio/to shadow the pipattiopackage.- App name resolves from
MODAL_APPenv var; falls back toelvis-ai-v2. - Image is debian_slim + Python 3.13 with a pinned subset of deps and local
libs/+src/mounted viaadd_local_python_source. - Secrets used:
apollo,attio,parallel(ModalSecret.from_name). - Free tier cap: 8 web endpoints total. Parallel endpoints are gated behind a plan upgrade.
- Endpoint modules are imported in
src/app.pyfor decorator registration — when adding a new endpoint module, add the import there.
Build env vars baked into the image: AI_BUILD_GIT_SHA, AI_DEPLOYED_AT.
Standalone Modal apps under webhooks/:
export_to_gcp_raw.py— raw payload sink to GCS (bucketdlthub-devx-test-bucket, secretdevx-gcp-202605111323).export_to_gcp_etl.py— ETL variant.
Deploy each independently with modal deploy webhooks/<file>.py.
OTEL traces emitted from libs/telemetry.py. Activated only when one of:
HYPERDX_API_KEY— direct HyperDX ingestion (auto-prefixesBearer).HYPERDX_OTLP_ENDPOINT— overrides the HyperDX endpoint (defaulthttps://in-otel.hyperdx.io/v1/traces).OTEL_EXPORTER_OTLP_ENDPOINT— custom OTEL collector.
If none set → tracer is a no-op. CLI calls init_tracer() at startup and emits cli.usage_error events on Typer exit code 2.
- Temp files:
tmp/only. Never alongside source. - Branches:
agent/<slug>for agent-created branches. Neverclaude/*. - Worktrees: under
worktrees/<branch-name>. Never use.git/modules/*paths as user-facing worktrees. - Commits: never add AI co-author trailers (
Co-Authored-By: Claude/Oz/...). Human authors only. - Documentation: live in code (docstrings, README per major module, CHANGELOG entries). Do not create summary/investigation
.mdfiles. - Path anchoring in scripts: anchor file I/O on
Path(__file__).resolve().parent, never the CWD —uv run path/to/script.pydoes not chdir.
pytestwith--import-mode=importlib(already inpyproject.toml).- Layout mirrors source:
tests/cli/,tests/libs/,tests/src/,tests/integration/. - Integration smoke:
tests/integration/test_gtm_remote_smoke.py. S101(assert) is allowed intests/**(ruff per-file ignore).
When adding functionality:
- External SDK call? → New file in
libs/<service>/. Wrap one SDK only. No cross-lib imports. - Multi-step flow / Modal endpoint? →
src/<service>/. Register module import insrc/app.pyif it defines endpoints. - User-facing command? →
cli/<group>/. Typer subapp. Call intosrc/. Wire intocli/main.pyviaapp.add_typer(...). - Standalone data product? →
data-gen/<product>/. Self-contained. - Webhook handler? →
webhooks/<name>.py. Independent Modal app.
See AGENTS.md (symlinked from CLAUDE.md) for the authoritative version of these rules.