Daily recaps with Cron, a Sandbox, and Claude Code
TL;DR: The v1 of this pulls my activity from Calendar, Gmail, Slack, Notion, and GitHub. It hands the blob to Claude Code running in a Vercel Sandbox, then distributes the result out to a Notion page, a private repo, and a Slack DM. A recap of my day (the commitments I made, things I learned, DMs I owe a reponse too, open threads that need my attention you know... all the things you get paid to do), shipped and recorded every evening.
Code lives here: github.com/ryanxkh/daily-recap. This post is the why behind the architecture, but the full source, schemas, and prompt template are in the repo (so you can just copy that repo link and build this much more quickly than I did).
Major shout to Drew Bredvick for the inspo. His build and breakdown can be found here
What this solves
- State loss between days. The recap compounds into a searchable archive of what I did, committed to, and learned.
- Unanswered asks. A
loose_endssection flags emails and DMs I didn't reply to. - Meeting prep tax.
tomorrow_on_decklists tomorrow's meetings with one-line prep notes (v1.5). - Selective attention. Hard rules in the prompt drop the noise — promo emails, security notifications,
@channelscroll-bys, CI/CD bot chatter — so I don't have to.
Architecture at a glance
Show codeHide code
Vercel Cron (0 23 * * * UTC — 6pm CDT / 5pm CST)
↓
/api/cron/recap (Next.js App Router, Node runtime)
↓ start() returns runId immediately
dailyRecapWorkflow (WDK, "use workflow")
↓
Step 1 — parallel prefetch (5 "use step" fns)
Calendar | Gmail | Slack | Notion | GitHub
each returns Result<T> = { ok: true, data } | { ok: false, reason }
↓
Step 2 — synthesize
Boot Vercel Sandbox from snap_xxx
Run `claude` headless with prompt + JSON schema
Extract + Zod-validate the Recap
↓
Step 3 — parallel fan-out
writeNotionPage(recap) → Notion Daily Briefs DB
writeArchiveMarkdown(recap) → private github repo commit
↓
Step 4 — sendSlackDM(TL;DR + Notion URL)
↓
Step 5 — log metadata
Any prefetch can fail without aborting. The workflow carries sources_degraded forward and the recap ships with a visible callout. A hard failure in synthesis triggers a Slack error-alert DM and aborts.
The one decision that mattered that really mattered was the prefetch taking place outside the sandbox.
The version of this I expected to build was going to be pointing Claude Code at a bunch of MCP connectors (Google, Slack, Notion, GitHub), let the agent loop figure out what to fetch, and have it write the recap. Basically whats on trend right now and is most likely titled "Agentic Chief of Staff" on someones substack post. However, after building something like that, and then seeing what Drew Bredvick had built, this Cron and Sandbox made so much more sense.
So, we do a little "fucking around and finding out" and landed on the below as my prefered solution. This workflow prefetches every source deterministically outside the sandbox using official SDKs and Claude only sees the assembled blob of it all. This means it only synthesizes that blob of information and doesn't actually fetch it.
Why this way?
- Faster and cheaper (I think). There is no tool-use round trips. The prompt goes in and the JSON comes out so just one single model call.
- Predictable. A deterministic prefetch layer with five
Result<T>outputs means I know exactly what Claude will and won't see. - Tokens stay out of the Sandbox. Google, Slack, Notion, and GitHub credentials never enter the microVM and the Sandbox only gets
ANTHROPIC_API_KEYand a prefetched JSON blob. - The Sandbox still earns its keep — Edge Functions are V8 isolates that can't run Node binaries at all, and Node Functions would fight the 250MB bundle cap plus reinstall the CLI on every cold start. A pre baked Sandbox snapshot sidesteps both with Linux microVM, Claude CLI already installed, and boots in a second or two.
If you're building a personal tool on top of Claude Code, the default MCP-everywhere pattern isn't always the right one. A "prefetch then synthesize" pipeline probably beats it for anything where the data sources are known and stable. This also makes building out new repeatable automations much easier in the future.
Stack
| Piece | Why it's here |
|---|---|
| Vercel Cron | Platform-native scheduler. Config lives in vercel.json. |
| Next.js App Router (Node runtime) | Hosts the cron route handler. |
| Vercel Workflow (WDK) | Durable orchestration. Each step retries independently, the run shows up in a trace, state survives crashes. |
| Vercel Sandbox | Firecracker microVM. Boots from a snapshot so the Claude Code CLI is pre-installed. |
Claude Code CLI (claude) | Agent loop, model access, JSON-schema output — all provided. I don't maintain any of it. |
| Zod | One schema drives the JSON schema passed to claude, the runtime validation, and the types every sink consumes. |
| Google / Slack / Notion / Octokit SDKs | Deterministic prefetch. Faster, cheaper, more predictable than MCP. |
Three failure modes I hit on first deploy
Each was a one-to-three-line fix, but each cost real debugging time.
1. "use workflow" silently no-op'd. I forgot to wrap next.config.ts with withWorkflow from workflow/next. The directive became a no-op. No build error — just a useless "invalid workflow function" at runtime. One-line fix.
2. VERCEL_SANDBOX_SNAPSHOT_ID missing in prod env. Worked locally via .env.local, blew up on Vercel the first time. Set it for both production and preview. One env add.
3. Claude wrapped the JSON in markdown fences. claude --output-format json --json-schema ... returns different shapes across CLI versions: sometimes root.structured_output, sometimes root.result as a string with ```json ... ``` fences around it. JSON.parse on the raw string fails. I added a stripMarkdownJsonFence + multi-path extractRecapJson to absorb the variation. Code in lib/sandbox.ts.
End-to-end first real recap was ~1m 36s, with the Slack degradation surfacing exactly as designed.

Implementation order, if I were doing it again
- Scaffold Next.js App Router. Add
withWorkflowinnext.config.tsimmediately — skipping step 1 of the WDK setup is how you get silent no-ops. - Write the Zod schema first. It's the contract between Claude, the workflow, and the sinks.
- Build the prompt against fake prefetch data. Iterate on selectivity and voice until you'd be happy reading the output.
- Set up external access: Google OAuth refresh token, Slack app, Notion integration + DB, GitHub PAT. All tokens go into Vercel env.
- Write one prefetch source end-to-end (Calendar is simplest). Lock the
Result<T>shape so the other four are copy-paste. - Write the remaining four sources.
- Bake the sandbox snapshot. Save the
snap_xxxto Vercel env. - Write the sandbox runner: boot snapshot, write prompt + schema via heredoc, run
claude, extract the JSON. - Write the sinks (Notion, GitHub archive, Slack).
- Wire the WDK orchestrator.
"use workflow"at the top of the file,"use step"on each step function. - Write the cron route. Verify
CRON_SECRET, callstart(). - Configure
vercel.json, deploy, trigger manually withcurlbefore trusting the schedule.
Trade-offs and known gaps
- Boxed in by the Claude Code CLI. If I ever need a planner/executor pattern with different models per step, this design doesn't bend without surgery. For a personal recap, that's a good trade.
- Snapshot lifecycle. Re-bake when Anthropic ships a new CLI version. Two minutes, one env change, easy to forget.
- Slack reads half-working (v1 gap).
search.messagesrequires a user token (xoxp-). v1 uses a bot token, so Slack search calls returnnot_allowed_token_typeand the section marks itself degraded. Recap still ships. - Tomorrow-on-deck empty (v1 gap). Calendar prefetch grabs today only; the tomorrow window is a v1.5 tweak.
- Octokit deprecation.
search.issuesAndPullRequestsis deprecated. Works today, will break eventually. - Single-fire assumed. No idempotency guard yet — if cron fires twice, you'd get two Notion pages. Fine for personal; a real product would key off
date + runId. - First-run setup is a lot. OAuth dance, Slack app, Notion schema, GitHub PAT, env, snapshot bake. 30–60 minutes the first time.
If you want to build this
The full implementation with the schema, prompt, workflow, sandbox runner, all five prefetch sources, all three sinks, the snapshot baker, and an ADMIN_SETUP.md checklist is in the repo github.com/ryanxkh/daily-recap. Fork it, or give the repo url to your vibe coding agent of choice (also can we stop calling it vibe coding and start calling it "agentic coding"?) and you'll have the same thing running in a couple of hours.