Anatomy of one run.

An email lands. About seven seconds and one model call later it's a labeled message with an audit row. Job-app email runs the graph for another twenty seconds into Notion.

01One run, timed

medians from the audit ledger, stored as JSONB
idempotency check
127 ms
gmail fetch
191 ms
classify (sonnet)
5 990 ms
apply label
579 ms
parse email
265 ms
sublabel (opus)
3 160 ms
apply sublabel
522 ms
notion match
12 751 ms
extract (opus)
4 013 ms
notion write
1 245 ms
010 s20 s≈30 s
triager job-apps graphstage medians, 805 + 14 production runs · label ≈7 s · full job-app pipeline ≈29 s

02Run artifacts

triager_runs · one row per run
{
  "message_id": "18f3…c2a",  // PK, dedups replays
  "primary_label": "Job Apps", "flagged": true,
  "model": "claude-sonnet-4-6",
  "input_tokens": 871, "cache_read_tokens": 4293, "output_tokens": 232,
  "cost_usd": 0.0074,
  "stage_timings_ms": { "fetch": 191, "classify": 5990,  }
}
services/ triager · job_apps · news_brief · morning_brief · spend_sync
shared/ db · gmail · auth · anthropic client · settings
site/ public site · reads neon directly, no API layer
tests/ 602 tests · run with zero secrets
migrations/ alembic
prompts/ ✂ redacted in the mirror

03Stack

boring infrastructure, on purpose

Backend

Python 3.12FastAPISQLAlchemy 2.0AlembicLangGraph

Frontend

Next.js 16React 19Tailwind v4

Data & models

Neon PostgresJSONB timingsSonnet 4.6Opus 4.7prompt caching

Ops

Modal apps + cronsLangSmith tracingPub/Sub push1Password secrets

04By the numbers

spend-sync reconciles these nightly
2,225emails classified · live
115briefs published · live
$0.010per email, prompt-cached
602tests · zero secrets

make test. 602 green.

The whole suite runs with zero credentials.

← fleet