Let's Talk
July 13, 2026Reading time: 7 min

Technical appendix — Part 2: How @scout briefs you before the meeting

The engineering companion to Part 2 — how the calendar poll stays free, how the forward-only firing window and the claim-it-first dedup guarantee exactly one brief, and how the OAuth token is encrypted. The shipped system, not a design doc.

MurtazaMurtaza - Senior Product Engineer

The engineering companion to "The Teammate That Briefs You Before You Ask." Everything below is the shipped system, not a design doc.


Two paths, one agent

@scout does two jobs that share a name and a database and almost nothing else. One is a deterministic poll that reads your calendar every few minutes and never touches an LLM. The other is an LLM writer that composes a briefing when there is a meeting worth briefing. The split is the whole design, and it is the reason "an AI that watches your calendar all day" costs nothing until it has something to write.

That boundary is also where the economics live. The thing that runs 24/7 (the poll) is free. The thing that costs money (the writer) runs at most once per meeting. When someone asks what an always-on calendar assistant costs, the honest answer is that the always-on part is not AI at all.

The poll path: read the calendar, decide, mostly do nothing

A cron schedule fires */5 * * * *, every five minutes. It is mapped to Scout in CRON_AGENT_MAP, and the dispatch is deliberately blunt: the scheduled handler bypasses the agent executor and the router entirely and calls pollCalendarReminders(env) directly. An earlier version routed the tick through an LLM whose only instruction was "call this tool immediately," which added latency and cost and zero value. The run is still logged to agent_runs, with model: null, zero tokens, zero cost. Observability survives the bypass; the free path is auditable, not invisible.

Inside the poll, per tick:

  1. Load enabled users, capped at 15 per tick and processed sequentially. The cap exists because each potential enrichment is an LLM call, and a Worker has roughly a 30-second CPU budget; fifteen sequential briefs is the safe ceiling before that wall. Beyond it, the honest answer is "scale this with Durable Objects," noted in the code as a known limit rather than pretended away.
  2. Refresh the access token if it is close to expiring (see encryption, below).
  3. Fetch upcoming events from the user's primary calendar only. Secondary and shared calendars are outside the query, which is exactly the blind spot the blog names.
  4. Apply the firing window (the interesting part, below).
  5. Claim, then enrich, then send (the other interesting part).

All-day events, cancelled events, and events with no ID are skipped early. No LLM has been touched yet.

The firing window: forward-only, once

A reminder is only useful in a narrow band of time, and the window math enforces exactly that. For each event, the target moment is eventStart − notificationMinutes (default 60, user-configurable from 5 to 1440). Then:

  • If the current time is before the target, skip. The window has not opened.
  • If the current time is more than ~5m30s past the target, skip. That is one cron interval plus a jitter tolerance; past it, the tick that should have fired already missed, and a late reminder is worse than none.
  • If the meeting has already started, skip. Scout never reminds you about a meeting you are already in.

The comment in the code puts it plainly: never fires before the target, never fires after the meeting starts. This is forward-only by construction. A missed tick is dropped, not deferred, and that is a deliberate trade: a reminder that arrives late is a reminder you have learned to distrust.

The claim that buys exactly-one

Two consecutive ticks of a */5 schedule can both land inside the same window. A naive check-then-send ("has this been sent? no? send it") lets both ticks pass the check before either records the send, and the user gets two identical DMs. This actually happened; the fix is worth stealing.

Scout claims the meeting in the database before it sends anything. The claim is an insert with onConflictDoNothing().returning() against a unique index on (slackUserId, googleEventId, meetingStartAt). The database, not the application, is the serialization point: exactly one concurrent insert wins and gets a row back; every other attempt gets an empty result and stands down. Only the winner sends the DM.

Two details fall out of that design:

  • The index includes the meeting's start time. A rescheduled meeting keeps its Google event ID but gets a new start, so it counts as a new row and earns a fresh brief. Move a meeting and you are reminded again, correctly.
  • The claim gates the spend, not just the DM. The LLM enrichment runs after the claim succeeds. A losing tick never reaches the model, so overlapping ticks cost nothing extra. The same trick that stops the double message stops the double bill. If the DM send fails after the claim, the row is deleted so the next tick can retry rather than being permanently marked done.

The enrichment: the only part that costs money

Once a meeting is claimed, enrichMeetingBrief runs. It is a single Sonnet call (claude-sonnet-4-6), capped at 400 output tokens and three steps: pull our own history on the company, optionally run one web search, write the brief. The output is the four sections from the blog. On any failure it falls back to a deterministic reminder built from the calendar data alone, so a meeting inside the window always produces something, even if the research half fails.

The company it researches comes from a guess, and this is the honest core of the "where it gets thin" section. extractCompanyName reads the first attendee whose email domain is not in a hardcoded set of consumer providers (gmail, outlook, and friends), using a hand-maintained list of two-segment TLDs so co.uk and com.au resolve correctly. If no business domain is found, it falls back to the first meaningful word of the meeting title, skipping a blocklist of generics ("weekly," "sync," "standup," "1:1"). If nothing survives that, it returns "No company identified from event" and no web search fires at all. This is pattern-matching on an email address, not comprehension of your calendar, and the system is built to admit that rather than confidently brief you on the wrong company.

Web search, and a note on names

The research uses Anthropic's provider-native web search tool (webSearch_20250305, capped at five uses per turn), surfaced through the AI SDK as a provider tool rather than something we hand-rolled. The system prompt requires a Sources section rendered as Slack links, which is why the brief comes with receipts. (You may spot the word "Tavily" in a stale comment or two; it is a fossil. The wiring is Anthropic.)

OAuth and encryption, briefly

Reading your calendar requires your consent, once. The OAuth flow requests read-only scope (calendar.readonly) and validates on the callback that read access was actually granted, erroring cleanly if it was not. It asks for offline access with a consent prompt specifically to receive a refresh token, which is the long-lived credential.

That refresh token is the only thing we store, and it is encrypted at rest with AES-256-GCM via the Workers-native Web Crypto API. The stored blob is the IV, the ciphertext, and the GCM auth tag concatenated and base64-encoded, under a 32-byte key held as a secret. If a decryption ever fails, for instance because the key was rotated, Scout treats the connection as broken: it disables it and DMs the user to reconnect, rather than silently failing to remind them.

The cost split, with the arithmetic

Deterministic poll, zero model tokens, runs all day. Sonnet writer runs once per meeting. Pay for reasoning only when there is a brief worth reasoning about.

Here is the writer's cost worked out, so the "a few cents" in the blog is a number and not a vibe. A brief is one enrichMeetingBrief call on claude-sonnet-4-6 (published pricing: $3 per million input tokens, $15 per million output), plus at most one Anthropic web search (published rate ~$10 per 1,000 searches, so ~$0.01):

  • Output: capped at 400 tokens → at most 400 ÷ 1,000,000 × $15 = $0.006.
  • Input: the system prompt, our own client history, and the search results, across the two model turns the web search costs — call it 3,000–8,000 tokens → roughly $0.01–$0.024.
  • Web search: one search → ~$0.01.
  • Total: roughly two to four cents per brief.

These are estimates from published prices and the code's real token caps, not metered figures — the real per-run numbers live in agent_runs (cost_microdollars) and will replace this once DB access returns. The poll, by contrast, is exactly $0: it records model: null and never touches a model, so no estimate is needed there.


Building something like this, or wondering whether the pattern fits your stack?

Back to the non-technical story.