Chapter 02 · Section III · 10 min read
2.3 Scheduled loops: runs on the clock
The third flavour of trigger — the clock. The loop fires at a time you picked, whether or not you're at your laptop. First real step into unattended work.
Scheduled loops fire on the clock. Every morning at 6:00, every Monday at 9:00, every fifteen minutes on the hour. You are not in the chair. Your laptop may be closed. The loop runs anyway because the trigger lives somewhere else — a cron server, a scheduled workflow, a hosted job — that doesn’t need you awake.
Why the trigger has to leave your machine
A live loop needs a session on your laptop. A run-until-done loop needs one command from you to start. A scheduled loop needs neither — which is only possible if the thing that starts it isn’t your laptop.
Three common places the clock lives:
- A cron server (or systemd timer). A machine you own, always on, running a scheduler that fires jobs at set times.
- A cloud scheduler. A hosted service (Cloud Scheduler, EventBridge, GitHub Actions cron) that fires a webhook or triggers a workflow at set times.
- A scheduling feature inside an agent product. The agent tool has a “run this daily at 6am” button and handles the machinery for you.
The choice matters mostly for one thing: what happens if the scheduler is down? A cron server on your laptop misses the 6am run if the laptop is off. A cloud scheduler doesn’t. Pick the one whose failure mode you can live with.
What changes when you leave
Everything you could paper over in live and run-until-done becomes real:
- Nobody catches the wrong output. If the loop emails a wrong summary at 6am, the first person to notice is the customer at 8am.
- Nobody notices when it doesn’t run. Silent failure — the scheduler didn’t fire, the job crashed, the model returned an error — will happen and you need to know.
- Nobody stops the runaway. If the loop hangs, retries forever, or wastes tokens, there’s no Ctrl-C. The budget (from 2.2) is your only stop.
- Nobody rescues the sandbox. If a cycle corrupts state, tomorrow’s cycle inherits the corruption. Chapter 3.1 (isolation) and Chapter 4 (memory) start mattering a lot.
None of this makes scheduled loops bad — most useful loops are scheduled. It just means the moment you cross from run-until-done to scheduled, you have to do the work you got away with skipping before.
Picking the schedule
The right cadence is usually one of a small number:
- Hourly / every few minutes. For work tied to fast-changing external state (a monitoring alert, a fast-moving inbox).
- Daily. For summaries, reports, digests, batch triage. The most common cadence.
- Weekly. For roll-ups, reviews, longer-cycle analyses.
- Monthly. For billing-adjacent work, quarterly-prep work, planning cycles.
Two anti-patterns:
- Every minute “just in case.” If the underlying state doesn’t change that often, you’re paying 60× more than you need to and drowning your logs. Match the cadence to the rate the input actually changes.
- Once a year. By the time the loop runs, nobody remembers if it works. Annual loops rot silently. Prefer monthly with a manual “the annual one” pass.
The idempotency question
Every scheduled loop must be safe to run twice. Networks glitch, schedulers double-fire, retries happen. If a second run of the same cycle produces a different outcome — sends a duplicate email, files a duplicate ticket, charges a duplicate invoice — that’s a bug, not “just unlucky.”
Two ways to make a cycle safe to re-run:
- Check before you act. Before sending, ask “has today’s summary already been sent?” If yes, skip.
- Use a deterministic key. When creating a record, give it an ID derived from the day + task, so a duplicate creation is a no-op or an update-in-place.
Idempotency is not glamorous work. It is what separates a loop you can leave running from one you can’t.
Reading the log the morning after
The morning-review habit: every scheduled loop should produce a single, glanceable “what did I do last night” summary that lands somewhere you’ll actually see (Slack, an email, a channel, a doc). Two rules:
- Silent success is fine — but only if you trust silent success. Most scheduled loops should post something every run, even just “ran cleanly; 12 tickets processed; nothing needed human review.”
- Loud on failure. If a cycle errored, the message should be impossible to miss, name the failing step, and (ideally) link to the log.
Check your understanding
Quick check
—