Anatomy of one run.

An email lands; ~3.5 seconds and two LLM calls later it's a labeled message, an updated Notion row, and an audit record.

01 / FIG. 2Timing diagram — one run

stored as JSONB on the audit row
webhook + jwt
128 ms
history fetch
412 ms
classify (llm)
887 ms
apply label
204 ms
dispatch
58 ms
langgraph (llm)
1 581 ms
notion update
257 ms
audit write
94 ms
01 s2 s3 s≈3.5 s
triager   job-apps   postgrestwo LLM calls · everything else is plumbing · end-to-end ≈3.5 s

02What every run leaves behind

triager_runs · one row per run
{
  "message_id": "18f3…c2a",  // PK — replays dedup
  "label": "Job Apps", "flagged": true,
  "model": "claude-sonnet-4-6",
  "tokens": { "in": 1842, "cached": 1496, "out": 31 },
  "cost_usd": 0.0041,
  "stages_ms": { "fetch": 412, "classify": 887,  },
  "outcome": "dispatched"
}
services/ triager · job_apps · news_brief · morning_brief · spend_sync
shared/ db · gmail · auth · anthropic client · settings
apps/web/ dashboard — reads neon directly, no API layer
tests/ 569 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 v4Tremor

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
1,190emails classified · live
31briefs published · live
$0.0041per email, prompt-cached
569tests · zero secrets

Clone it. make test. 569 green.

The suite runs with no credentials at all — that's the point.

← fleetread the code ↗