Drew Bredvick

Building the future of GTM with AI

← Back

Claude Code as a Cron Job

·4 min read

I built a daily recap agent in about an hour. It reads my Granola meeting notes and Slack messages, synthesizes them, and appends a summary to my Reflect daily note. Every day at 6pm, automatically.

The whole thing is ~200 lines of code. Most of it is boilerplate.

The bitter lesson

Rich Sutton's bitter lesson in AI research is that general methods leveraging computation beat hand-crafted approaches every time.

We're making the same mistake with agents.

Most agent code today is custom tool-calling loops, hand-rolled state machines, bespoke orchestration, elaborate prompt chains. Our lead agent at Vercel is a great example. It works really well. It also has a lot of code. Should it just be Claude Code with Opus, a few MCPs, and a couple skills? Probably.

Every time the underlying model gets better, all that scaffolding becomes dead weight.

The pattern

Vercel Cron (6pm daily)
  -> GET /api/cron/daily-recap
    -> boots a Vercel Sandbox from a snapshot
      -> Claude Code runs with a plain English prompt
        -> reads Granola meetings via MCP
        -> reads Slack messages via MCP
        -> appends recap to Reflect via skill
      -> returns structured JSON output

That's it. Claude Code is the agent. It already knows how to use tools, recover from errors, and reason about multi-step tasks.

The sandbox is a Vercel Sandbox snapshot with everything pre-installed: Claude Code CLI, Granola MCP server, Slack MCP server, and a custom Reflect skill. Boot time is a few seconds.

What the code looks like

The cron endpoint just kicks off the workflow and returns immediately:

import { start } from 'workflow/api';
import { workflowDailyRecap } from '@/workflows/daily-recap';

export async function GET(request: Request) {
  const authHeader = request.headers.get('authorization');
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  await start(workflowDailyRecap, []);
  return NextResponse.json({ success: true });
}

The workflow boots the sandbox and runs Claude Code with a prompt:

const SNAPSHOT_ID = 'snap_HdhfHapYC64q4csoobiujDxXfhVW';

export async function runDailyRecap(): Promise<DailyRecapOutput> {
  const today = new Date().toLocaleDateString('en-US', {
    weekday: 'long', year: 'numeric',
    month: 'long', day: 'numeric',
    timeZone: 'America/Chicago',
  });

  // Boot sandbox from snapshot -- CC, MCPs, and skills are pre-installed
  const sandbox = await Sandbox.create({
    source: { type: 'snapshot', snapshotId: SNAPSHOT_ID },
    timeout: ms('10m'),
  });

  const prompt = buildDailyRecapPrompt(today);

  await sandbox.writeFiles([
    { path: '/tmp/prompt.txt', content: Buffer.from(prompt, 'utf-8') },
    { path: '/tmp/schema.json', content: Buffer.from(JSON_SCHEMA, 'utf-8') },
  ]);

  // That's it. Just run Claude Code.
  const result = await sandbox.runCommand({
    cmd: 'sh',
    args: ['-c', [
      'claude',
      '--dangerously-skip-permissions',
      '--output-format json',
      '--json-schema "$(cat /tmp/schema.json)"',
      '-p "$(cat /tmp/prompt.txt)"',
      '> /tmp/output.json',
    ].join(' ')],
  });

  const output = await sandbox.readFileToBuffer({ path: '/tmp/output.json' });
  return DailyRecapOutputSchema.parse(
    JSON.parse(output.toString('utf-8')).structured_output
  );
}

The prompt is just English:

You are Drew's personal assistant doing an end-of-day recap.
Today is {date}.

Your job:
1. Review today's Granola meeting notes
2. Review today's Slack activity involving Drew
3. Synthesize into a concise daily recap
4. Append the recap inline on today's Reflect daily note

Include Slack links when possible. Be selective -- only things
Drew needs to know about or act on.

Claude Code already has access to the MCP servers and skills in the snapshot. It figures out how to use them.

Why this works

Three things:

Snapshots preserve auth state. Configure the sandbox once (install MCPs, auth everything, add skills), save a snapshot. Every boot comes up fully authenticated and ready to run.

MCPs are a shared interface. Granola has an MCP server. Slack has an MCP server. These are the same protocol Claude Code uses locally on my laptop. The agent in the sandbox connects to data sources the same way I do during my workday.

It gets better for free. When Anthropic ships a smarter model, my daily recap gets smarter. The prompt stays the same, the MCPs stay the same, the output schema stays the same. The agent just makes better decisions.

Isomorphic agents

The architecture is the same whether the agent runs as a cron job in a sandbox or sits in my terminal while I work. Same Claude Code, same MCPs, same skills. My local dev setup is the production agent.

This means I can prototype workflows by just doing them. If I find myself running the same Claude Code prompt every evening, that's a workflow I should automate. Copy the prompt, make sure the MCPs are in the snapshot, add a cron trigger. About 200 lines of infrastructure to go from manual to automated.

What I'm building next

The daily recap is the first workflow. A few more I'm thinking about:

  • Standup to tickets. I Granola my standup every morning. When I mention something we should build, create a Linear ticket with full context from the meeting notes. This is the "ticket from conversation" pattern.
  • Ticket to code. Take a Linear ticket, combine it with relevant Notion docs and the GitHub repo, and generate a PR. Friday afternoon, review a batch of PRs that the agent produced during the week.
  • Morning briefing. Before standup, append today's calendar, open PRs, and Linear sprint progress to my Reflect daily note. Walk into the meeting already knowing the state of things.

Same architecture for each: sandbox snapshot + Claude Code + MCPs + prompt + trigger.

Get started

If you want to try this yourself:

  1. Create a Vercel Sandbox and install Claude Code
  2. Connect whatever MCP servers you need (Slack, GitHub, Linear, whatever)
  3. Save a snapshot
  4. Write a Next.js route that boots from the snapshot and runs claude -p "your prompt"
  5. Add a cron schedule in vercel.json

The Vercel Sandbox Claude starter is a good starting point for the boilerplate.

My current working theory is that almost no one is using Claude Code this way yet. I think it'll become the default for lightweight agent workflows.

Stay up to date

Get essays and field notes delivered as soon as they publish.