The Itch
Every morning, same ritual. Open Claude, ask it to summarize my recent pull requests, check for blockers, prep a standup. Three minutes, every single day.
It’s not a lot of time. But it’s the kind of time that bothers a developer — repetitive, predictable, mechanical. Claude has a CLI. The CLI can run unattended. What if cron just… did this for me?
The Learning Loop
These past few months — learning new tools, new patterns, new ways of working with AI — and the thing that keeps surprising me is the speed. Not the speed of the AI itself, but the speed of the development loop. An idea I’d have over morning coffee could be a working prototype by the time I close my laptop that evening. Things that would have been a week-long side project — spread thin across stolen hours — now materialize in a single focused session.
This isn’t groundbreaking. Plenty of people have built their own versions of “autonomous Claude”: cron wrappers, custom schedulers, Claude Code extensions, full-blown agent frameworks. Some use existing tools like OpenClaw, others write bash scripts, others build elaborate multi-agent systems. The space is full of experimentation, and there’s no canonical answer yet.
What I’m sharing here is my version — a “routine desk” that lets me define, schedule, and monitor AI tasks through a simple dashboard. It’s the story of building it, what I learned, and the specific design choices that made it useful. Your version would look different, and that’s the point. The interesting part isn’t the artifact — it’s what you discover along the way.
The Simplest Version
The first attempt was exactly what you'd expect:
0 9 * * 1-5 claude -p "Summarize my recent code changes" > /tmp/standup.txt
It worked! Sort of. For about two days.
Then the problems compounded. Monday morning: the output file was empty because the CLI had hit an authentication issue overnight — cron swallowed the error silently. Tuesday: two runs overlapped because the first one took 8 minutes instead of the usual 3, and the second kicked off on schedule before the first finished. By Wednesday I had six temp files named standup.txt, standup2.txt, standup-final.txt... you know how that goes.
No visibility into whether runs succeeded or failed. No error handling. No history. No way to tell at a glance whether the system was healthy or broken. Cron is a fantastic tool for running deterministic commands. An AI CLI call is not deterministic — it can hang, timeout, produce unexpected output, or fail silently. I needed something that understood that.
I needed something small but proper.
Routines as Markdown
Here's the core insight that shaped everything: a routine is just a prompt plus scheduling metadata. And there's already a perfect format for "structured metadata + freeform text" — Markdown with YAML frontmatter.
Here's what my morning standup routine looks like:
---
title: "Morning Standup Summary"
schedule: "0 0 9 * * 1-5 *"
model: sonnet
timeout: 300
max_turns: 50
---
Generate a morning standup summary. Do the following:
1. **Recent PRs**: Search for my recent pull requests from the past 24 hours.
List each with its title, status, and a one-line summary.
2. **Active Tasks**: Search for my active tasks. List each with its title,
priority, and current status.
3. **Blockers**: Identify any code reviews awaiting approval or tasks that are blocked.
4. **Today's Focus**: Based on the above, suggest 2-3 priorities for today.
Format the output as a clean markdown summary suitable for posting
in a team chat.
The filename is the routine name. morning-standup.md creates a routine called morning-standup. Drop a file in the directory, it gets scheduled. Delete it, it stops. Set enabled: false in the frontmatter to pause it without removing it.
Each routine picks its own model. Anthropic's Claude models range from fast and cheap (Haiku) to powerful and expensive (Sonnet, Opus) — and different tasks need different trade-offs. My standup uses Sonnet — it needs to search across code changes and synthesize a report, so reasoning quality matters. My health-check routine uses Haiku — it's a quick status ping, optimized for speed and cost. A Haiku call costs a fraction of Sonnet and returns in seconds. When all you need is "are things on fire? yes/no," you don't need the most powerful model:
---
title: Metrics Health Check
schedule: "0 0 8 * * * *"
model: haiku
timeout: 120
max_turns: 30
---
Perform a quick health check on key operational metrics...
The Pivot and The Engine
I originally planned to build this in TypeScript. The Claude Code SDK has a clean query() API, node-cron handles scheduling, node:sqlite handles persistence. I had a reference implementation to model after — a review agent built on that exact stack. I started building.
Then I hit a wall: the package registry wasn't accessible in my locked-down dev environment — no outbound network access to install packages. No npm install, no dependencies, dead end.
So I pivoted. Rewrote the whole thing in Rust.
What started as a constraint became the best architectural decision of the project. The final artifact is a single binary — async with Tokio, embedded SQLite via rusqlite, zero runtime dependencies. You can copy it to any machine and run it. No node_modules, no package manager, no runtime version to match. Just one file that contains the scheduler, the executor, the database, and the dashboard. In hindsight, this is exactly what you want for infrastructure that's supposed to run unattended.
The Executor is the core loop. It spawns the claude CLI as a subprocess, streams stdout and stderr concurrently via tokio::select!, counts messages and tool uses as they stream by, and enforces a hard timeout:
let result = timeout(timeout_duration, async {
let mut child = cmd.spawn()?;
let mut stdout_reader = BufReader::new(child.stdout.take()?).lines();
let mut stderr_reader = BufReader::new(child.stderr.take()?).lines();
loop {
tokio::select! {
line = stdout_reader.next_line() => {
match line {
Ok(Some(line)) => {
message_count += 1;
if line.contains("tool_use") { tool_use_count += 1; }
output_lines.push(line);
}
Ok(None) => break,
Err(_) => break,
}
}
line = stderr_reader.next_line() => { /* log and continue */ }
}
}
// ...
}).await;
The Scheduler creates one async task per routine. Each task parses its cron expression, calculates the duration until the next trigger using chrono::Utc::now(), sleeps for exactly that duration, and fires the executor. This means each routine runs in its own Tokio task — no central polling loop, no priority queue, no timer wheel. Each routine is independently responsible for waking itself up.
Overlap protection is a HashSet behind a mutex — before running, check if the routine name is in the set. If it is, skip this tick. Five lines that prevent the most common cron footgun:
{
let mut locks = locks.lock().unwrap();
if locks.contains(&name) {
tracing::warn!(routine = %name, "Already running, skipping tick");
continue;
}
locks.insert(name.clone());
}
Crash recovery is equally minimal. On startup, a cleanup_stale_runs() function queries SQLite for any runs with status running or pending and marks them as failed. If the process crashed mid-run, those records would be stuck forever without this. Five lines that handle unclean shutdowns.
Hot-reload uses the notify crate to watch the routines/ directory. When a .md file changes, the watcher reloads all routines and updates the scheduler. There's one pragmatic hack worth noting: std::mem::forget(watcher) — leaking the watcher so it lives for the program's lifetime rather than getting dropped at the end of the setup function. In a "proper" codebase you'd store the watcher handle somewhere and drop it on shutdown. Here, the program runs until you kill it, so leaking is functionally correct and saves a bunch of lifetime gymnastics. Pragmatism over purity.
The full engine — foundation, scheduling, output handling, dashboard, polish — was built and working in a single sitting. That's the compressed development loop I mentioned at the start: what would have been a multi-week side project materialized in one focused evening with Claude as a pair programmer. Rust's compiler caught entire categories of bugs at compile time that would have been runtime surprises in TypeScript. The borrow checker is annoying until it's saving you from a data race in your async scheduler — then it's your best friend.
┌──────────────────────────────────────────────────────────────┐
│ Claude Routines Engine │
│ │
│ ┌──────────┐ ┌────────────┐ ┌──────────────────────┐ │
│ │ File │ │ │ │ Executor │ │
│ │ Watcher │───►│ Scheduler │───►│ ┌────────────────┐ │ │
│ │ (notify) │ │ (cron) │ │ │ claude -p "..." │ │ │
│ └──────────┘ │ │ │ │ --model sonnet │ │ │
│ ▲ │ ┌────────┐ │ │ │ --max-turns 50 │ │ │
│ │ │ │Overlap │ │ │ └────────┬───────┘ │ │
│ routines/*.md │ │ Lock │ │ │ │ │ │
│ │ └────────┘ │ │ stdout/stderr │ │
│ └────────────┘ └──────────┬─────────┘ │
│ │ │
│ ┌────────────┐ ┌──────────▼─────────┐ │
│ │ Dashboard │ │ SQLite Store │ │
│ │ :3456 │◄──►│ (WAL mode) │ │
│ │ │ │ runs, status, │ │
│ └────────────┘ │ crash recovery │ │
│ └────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Dashboard
The dashboard is built on raw TCP — no web framework, no Axum, no Actix. One file that parses HTTP requests by hand, routes them, and generates the entire HTML as a Rust string. TcpListener::bind, tokio::spawn per connection, pattern-match on (method, path). That's the entire web server.
Why no framework? Because adding a dependency means adding complexity, and this dashboard needed to do exactly four things: show routine cards, show run history, show logs, and trigger manual runs. A framework would have been architecturally correct and practically overkill.
It serves a dark-themed single-page dashboard: routine cards with status dots (green for last-run-succeeded, red for failed, gray for never-run), a run history table with timing and status for each execution, and a log viewer that shows the raw Claude output for any run. Everything renders server-side — the HTML is a giant Rust string interpolation. No client-side framework, no hydration, no build step.
The killer feature is the "New Routine" button. It opens a modal where you define a routine — title, schedule, model, prompt — and hitting save sends a POST to /api/routines. The server writes a .md file to the routines directory. The file watcher detects the new file. The scheduler picks it up and starts running it. The system grows itself from the browser. You don't need SSH access or a text editor to add a new routine — just a browser and an idea for what Claude should do next.
Browser ──POST /api/routines──► Dashboard Server
│
writes metrics-check.md
│
▼
routines/ directory
│
File Watcher detects
│
▼
Scheduler reloads
& schedules new routine
No CDN, no build step, no external CSS. Auto-refresh with setInterval every 30 seconds, paused when modals are open so your form doesn't disappear mid-edit. The entire UI — CSS, JavaScript, HTML template — is self-contained in the binary. cargo build --release and you have everything.
The "Aha" Moment
The first morning I woke up to a standup summary already sitting there — generated at 9 AM while I was making coffee — something shifted. I didn't open Claude and ask it to do something. It had already done it.
My metrics check ran at 8 AM and flagged an issue before I'd opened my laptop. The notification was waiting. The context was there. I just needed to act on it.
It's a small thing, objectively. A cron job that calls Claude. But subjectively, it feels different. The mental shift is from "I use Claude" to "Claude works for me." The tool has agency — bounded, scheduled, observable agency, but agency nonetheless.
And it changes how you think about your own time. Those three minutes I spent every morning preparing a standup? Gone. But it's not just three minutes saved — it's the cognitive overhead of remembering to do it, the context-switching cost of opening a new session, the friction of formulating the same prompt for the hundredth time. All of that evaporates. You just... have the summary. You open your laptop and the work is already done.
There's a compounding effect too. Once the system exists, the marginal cost of adding a new routine is basically zero — write a markdown file, drop it in the directory. So you start thinking about what else could run unattended. Weekly report summaries. PR review reminders. Dependency update checks. Each one is a few paragraphs of markdown. Each one saves a few minutes a day. The minutes add up. Before long you have a small fleet of routines quietly doing work in the background, and you're spending your own time on the things that actually require you.
Limitations and What Comes Next
For all that it does, routines have real constraints. Every run is stateless — each execution starts fresh with no memory of previous runs. Routines can't coordinate with each other. They can't decompose complex work into smaller pieces. They don't learn from failures.
If a routine fails, it just... fails. There's no retry logic, no fallback strategy, no way to say "try a different approach." The SQLite store records what happened, but nothing acts on that information automatically.
These are fine constraints for scheduled tasks. A daily standup doesn't need memory. A metrics check doesn't need to coordinate with anything. But real work — the kind that takes hours, requires research across multiple areas, produces structured deliverables, and has pieces that depend on other pieces — needs something more.
I started with a question: "What if cron ran Claude for me?" That question led to a routine engine. But the engine surfaced a bigger question: "What if Claude could manage its own work?"
I had a system that could run tasks. But I wanted a system that could manage work — decompose it, prioritize it, retry intelligently, and know when to ask for human help.
That's where Part 2 picks up: a task executor with planners, workers, dependency graphs, retries, and a feedback loop where AI personas negotiate with each other about how to approach a problem. The system that routines couldn't be.
The broader ecosystem is moving this direction too. Tools like Claude Code are enabling a wave of builders to experiment with autonomous AI workflows. There's no canonical way to do this yet — which is what makes it exciting to build.
Part 2: "From Routines to a Crew — Building an AI Task System That Plans Its Own Work" explores what happens when you give Claude the ability to decompose, plan, and coordinate its own work.

Top comments (0)