The engineering companion to "We Were Drowning in Slack. So We Built an AI Team to Run Our Studio." Everything below is the shipped system, not a design doc.
The shape: one Worker, no framework
The entire system is a single Cloudflare Worker. One entry point exports fetch (Slack events, GitHub webhooks, Google OAuth callback, /health) and scheduled (cron). No LangChain, no CrewAI, no agent framework. The agent runtime is three files:
Component | Job | Size |
|---|---|---|
| Slack commands/mentions/DMs, cron ticks, and GitHub webhooks → one | 74 lines |
| decide which agent handles the event | 62 lines |
| build the tool set, run the LLM loop, return a result | 158 lines |
That's under 300 lines of runtime. The rest of the codebase is agents, tools, and the memory layer. Why so deliberately small? Because when a loop misbehaves at 9 AM, we can read the whole loop. (We wrote a full post's worth of war stories on exactly this; link when published.)
Everything is channel-agnostic by design: Slack is v1, but agents never see Slack. They see a NormalizedEvent. Teams or email later means writing an adapter, not touching an agent.
Routing: deterministic first, LLM second
Slash commands and cron ticks carry an explicit agent hint and never touch an LLM to route. /pulse goes to Pulse, period. Only free text (@mentions, DMs) gets classified, by Haiku, the small cheap model, which picks one agent or asks for clarification. DMs fall back to your last-used agent.
The principle: only pay for reasoning when there's a decision to make. It shows up again below in the cost section.
The LLM loop
We use the Vercel AI SDK (generateText with tools) rather than a provider SDK directly. Swapping the model provider is a one-file change. Current split:
- Claude Sonnet for agent work (the actual reasoning),
- Claude Haiku for routing and memory distillation (classification-shaped jobs).
The loop is bounded: 5 tool-use steps for interactive commands, 30 for cron jobs. Interactive runs are hard-capped at 25 seconds (Workers' limit is ~30s). On timeout the user gets an honest "that took too long" with a Retry button, and the in-flight LLM call is cancelled via AbortSignal, not orphaned.
Slack's rule is ack-within-3-seconds; all real work happens after the ack in ctx.waitUntil.
The shared brain
All three agents read and write one Postgres database (Neon, reached through Cloudflare Hyperdrive). The knowledge layer is built up in tiers:
- Provenance: every decision and action item records where it came from: which message, which meeting, who triggered it.
- Embeddings: 768-dimension vectors (Workers AI,
bge-base-en-v1.5) on decisions, summaries, and meetings, stored in pgvector columns. - A unified facts view: decisions, action items, project summaries, and meeting notes ranked into one feed. When a project or client is in scope, a ~500-token excerpt is injected into the agent's context automatically.
- Entity resolution: "John", "john d", and "John from Acme" resolve to one canonical person via an alias table, with interactive Slack disambiguation when it's genuinely ambiguous. Identity is deterministic-first: similarity search suggests, the alias table decides.
/ask: natural-language querying over all of it, with sources.
Per-user memory is separate: Haiku distills durable facts about each person from conversations, retrieved later by embedding similarity. One security-relevant detail: recalled memories are injected as user-role turns, never system-role, so a poisoned "memory" can't impersonate the system prompt (stored prompt injection mitigation).
Observability: the ledger the blog bragged about
Every LLM invocation, router calls included, writes a row to an agent_runs table: trigger, timing, model, tokens in/out, tools used, tool errors, success/error type, and cost in microdollars (1 = $0.000001), computed from per-token pricing at write time.
Two design rules worth stealing:
- The run log is the last thing allowed to fail. Every code path that invokes an LLM produces exactly one row, including timeouts and errors. A dashboard that only logs successes is a dashboard that lies.
- The daily cost report costs nothing to produce. A cron aggregates yesterday's spend by agent and posts it to the ops channel. Pure SQL, zero LLM calls. Observability that pays for itself.
The same "don't pay for non-decisions" principle killed our biggest cost line: a 5-minute calendar poll originally ran the full agent loop (288 LLM calls/day whose only job was to call one tool). The poll now bypasses the LLM entirely and only wakes Sonnet when there's an imminent meeting to brief, while still logging a run row every tick so the observability survives the bypass.
Safety guardrails (Episode 4 will go deep)
- Raw tool errors never enter LLM context. A failed tool returns an opaque "Tool execution failed" to the model; the real error goes to the run log. This blocks both prompt injection via error strings and secret leakage through stack traces.
- Raw errors never leave unauthenticated HTTP responses either (
/healthis boring on purpose). - GitHub access is a read-only GitHub App, scoped to our own org's repos.
- OAuth refresh tokens are AES-256-GCM encrypted at rest; per-user API keys likewise.
- Cron jobs are idempotent. Unique indexes make "the cron fired twice" a non-event instead of a duplicate-message event.
Deployment
Two environments, prod and dev, each its own Worker and its own Slack app, so we can break dev in front of nobody. Deploys are wrangler deploy; secrets live in Cloudflare, never in the repo.
Building something like this, or wondering whether the pattern fits your stack? Get in touch
Back to the non-technical story: Read Episode 0