What actually schedules an agent?
A scheduled agent is just a headless run on a timer, and that timer can sit in one of three places. They differ mostly in how much they handle once the trigger has fired.
- The operating system, wrapped around any headless agent. cron on Linux, launchd on a Mac, or a scheduled workflow in your CI all do the same thing: run a command on a clock. Point one at an agent’s headless mode, or at your own script around its SDK, and you have a scheduled agent. The scheduler starts the job and reads nothing else, so everything that keeps the run safe is yours to add on top.
- The agent’s own scheduler, where it ships one. Some tools put scheduling in the box. Anthropic’s Claude Code runs jobs two ways: scheduled tasks on your own machine, and hosted routines that fire with your laptop shut. A few of the newer agent frameworks ship cron schedules in the box too. Not every tool that starts its own runs works off a clock: LangChain’s Open SWE fires on a mention in Slack, Linear or GitHub rather than on a timer. And some ship nothing here at all: OpenCode’s headless server waits to be called, and Pi, the minimalist agent, leaves the timer to whoever runs it.
- A workflow or control plane, where the schedule is one trigger on a process you’ve declared. Here the clock isn’t a line in a crontab, it’s a property of a process written down: this runs nightly, in these stages, under this budget. The schedule and the run’s guardrails live in the same place, so the thing that fires the job is also the thing that bounds it.
What breaks once the run is unattended?
Everything that makes an unattended run risky shows up after the trigger. A bare scheduler fixes none of it; a control plane packages these controls with the trigger, but the clock itself provides no safety either way. A background agent earns its reliability from the layer wrapped around the clock.
- Spend outruns its own report. You can cap a single run, but usage reporting lags the real spend, so any dollar figure the job sees is already behind. One Claude user’s $6,000 night is the worked case: a command that re-ran 46 times over 26 hours, nothing above it to stop the re-fire, and a dashboard that stayed days behind the money. A budget enforced outside the run and checked before each model call beats a number the run reads about itself. Agent spend budgets covers why a cap that stops the run and a meter that reports on it are two different tools.
- The run draws on capacity you can’t see. A job authenticated with a personal subscription spends against the same allowance as your own interactive sessions, and the plan’s exact limits usually aren’t spelled out in tokens, so the job and you compete for a ceiling neither of you can read. A metered key drops the shared ceiling and hands you a bill that climbs with every call instead. Either way the run is drawing down capacity you were relying on, and which kind you’re on decides what a runaway night actually costs.
- The trigger quietly misses. Sometimes the job doesn’t fire at all: a typo in the crontab, a daylight-saving shift that moves 2am, a scheduler that never came back after a reboot. Bare cron doesn’t check that the run it scheduled actually happened, so nothing errors and the miss stays silent until you go looking for output that isn’t there. A heartbeat or deadline alert closes that gap by watching for the run that never checked in, and it’s one more thing you wire up yourself.
- A half-finished run can’t take back what it already did. A run that dies after firing one of three notifications leaves that one sent, and a plain retry sends it again. Scheduling never undoes an external action, so a job that writes anywhere real has to be safe to run twice before it runs alone. That property is called idempotency, and building it in is the operator’s job in every setup, hosted or not.
- A run can finish clean and still be wrong. Exit status only proves the process ended; whether the result is any good takes a check you write per job. A scheduled run has nobody to ask, so a clean exit on bad output just sits there until someone reads it. Questionable output needs somewhere to go: a defined place a stuck or unsure run escalates to and waits, one a person actually checks.
A minimum cron wrapper
For a job only you depend on, cron is often the right call. Four pieces have to sit around the run:
- A lock, so a slow night doesn’t stack a second run on top of the first.
- A timeout, so a hung run gets killed instead of sitting there forever.
- Captured output, written somewhere instead of vanishing with the process.
- A rerun-safe body, so a retry doesn’t repeat what the last run already did.
The schedule and the first three fold into one line pointing at a small wrapper:
# 02:00 daily. One run at a time, killed after 20 minutes, output to your home dir.
0 2 * * * /usr/bin/flock -n /tmp/agent.lock /usr/bin/timeout 20m "$HOME/agent/run.sh" >> "$HOME/agent.log" 2>&1
flock -n skips the run when the last one is still going; timeout kills a hung one. The same line schedules any headless agent; only the wrapper it calls knows which one. That wrapper sets up what cron won’t:
#!/usr/bin/env bash
set -euo pipefail
# cron's PATH is bare; add wherever your agent's binary lives (check `which`).
export PATH="$HOME/.local/bin:$PATH"
export AGENT_TOKEN="$(cat "$HOME/agent/token")"
cd "$HOME/reports"
your-agent -p "summarize yesterday's errors; open one issue if anything regressed"
curl -fsS "$HEARTBEAT_URL" # ping a dead-man monitor last; a missed ping alerts you
Cron runs with almost no environment, which is what trips people up. The token your shell profile exports won’t be set, and a user-local install won’t be on the PATH, so point the wrapper at whatever which resolves. Cron also doesn’t start in your repo, so you cd there yourself. The final line pings a dead-man monitor once the run finishes, so a night that never fired, or died before the ping, trips an alert instead of passing in silence.
Wired up this far, the wrapper still leaves three jobs to you:
- A hard spend cap. It can bound a run’s turns and its minutes; dollars are out of its reach. A spending limit has to live inside whatever makes the model calls and be able to refuse the next one, which a cron line can’t touch.
- A check on the result. It starts the job and never reads what came back, so a clean exit on bad output looks exactly like success.
- Output that finds you. A logfile isn’t somewhere you’ll look on a normal morning; pipe the summary to mail or a chat channel when the run is one you need to hear about.
For a read-only summary you check yourself, the wrapper is enough, provided a spend limit is already enforced somewhere: the provider’s account cap, or the plan’s own ceiling. It’s also every line you now maintain. The lock, the timeout, the PATH fix and the token handling each work until the day a tool underneath changes, and then the wrapper needs a patch nobody scheduled. Seven jobs every DIY agent harness ends up owning walks the full bill, and where each piece stops being worth building yourself.
What changes when a team depends on the run?
A personal scheduler assumes one person’s setup: your login, your machine, your plan. That’s the right size for a job only you rely on. When a team depends on the same run, the parts that were fine as yours stop being fine as everyone’s.
Start with the credential. Anthropic documents a token for scripts and CI, so one person’s own scheduled job runs fine on a personal subscription. That token stays bound to a single account, so the plan behind it can’t be handed to teammates or to a service acting for them. The consumer terms put it plainly: you can’t share your account or make it available to anyone else. Ride a team’s automation on one person’s login and you’re either outside the terms it runs under or one resignation away from a dead pipeline.
Capacity carries the same problem, now with shared team work draining one person’s unpublished limits.
Teams that outgrow this move the scheduled work onto organization API credentials, billed per token instead of against anyone’s plan. That swaps a fixed subscription for a meter, and the meter changes how you build.
Once every call has a price, the model you pick per step is a spending decision. A step that renames files or summarises a log doesn’t need frontier judgment; the expensive model earns its keep on the step that actually decides something. You can route the models by hand, since subagents and SDK calls each take their own. What you’d otherwise build for yourself is the version that holds that mix under one cap: claude-opus-5 on the judgment stage, deepseek-v4-pro on the routine one, and a single declared budget enforced across the whole run.
And the run history stops being yours to keep in your head. A job only you depend on can live in your scrollback. A job the team depends on has to stay readable after you’ve moved on, so whoever inherits it can see what it did last night and what it’s been doing all month.
How much machinery does the run actually need?
How much a scheduled run needs depends on what it can spend and what it can break. A read-only job that summarises yesterday’s logs barely needs more than the cron wrapper above. A job that writes to production or moves real money needs most of what follows. Size the controls to the damage a bad run could do:
- A spend ceiling enforced outside the run. A budget the run can’t move, checked as it spends rather than reported after, so a lagging usage number can’t overshoot it.
- A boundary sized to what the run can reach. The more a run holds in credentials, tools and data, the more it should be walled off from your laptop and the rest of the host. A container, a scoped user, or a fresh isolated VM per stage each draw that line; which you need depends on what a bad run could touch.
- Credentials scoped to the stage. Each step runs with the access its job needs and nothing wider, so a bad prompt can only reach what that step could already reach.
- Checkpoints between stages. After a crash or a tripped limit, the run restarts from the last finished stage instead of from zero. It still can’t un-send what a half-finished stage already did; making each stage safe to run twice stays your design work, in any architecture.
- A stop rule and somewhere to escalate. “Blocked” is a state the run can enter, and it lands somewhere a person actually checks, with enough context to decide in a minute. An agent inbox is that somewhere.
- A record you can answer for. The run keeps its inputs, outputs and approvals on the record, cost alongside, so you can account for it a week later from your phone.
You can build these yourself or adopt a system that already has them. Sumn is one such control plane: it runs each stage in its own isolated VM and checkpoints between them, so a killed run restarts at the last finished stage. It enforces the spend cap as the run goes, pausing warm for a person at the limit instead of pushing past. Credentials stay scoped per stage, blocked runs wait in an attention inbox someone checks, and every run leaves a receipt you can read a week later.
It’s also a hosted dependency and a metered bill of its own, which for one person’s low-stakes nightly job is more than the job needs. Those controls start earning their keep when a failed run costs real money or touches something other people rely on. That’s the same line afk-agents draws between a run that looks unattended and one that’s safe to leave.