Agents that run when nobody asked.
A scheduled agent has no one to ask, which flips your defaults: every ambiguity must resolve to "do nothing and say so", and the failure mode you will actually ship is silence rather than an error. The hardest thing on this page is not the cron expression or the retry — it is deciding, on each firing, whether what happened is worth a human's attention, because an agent that pings every run gets muted in a week and then it is not running at all.
Remove the user and three defaults invert.
An interactive agent has a cheap escape hatch for every hard moment: ask. Take the user away — put the agent behind a cron entry, a webhook, a queue or a file watcher — and that hatch closes while every other property stays the same. Three habits that are correct in an interactive loop become wrong here.
- Ask becomes abstain. When the agent cannot resolve an ambiguity, the interactive answer is a clarifying question and the unattended answer is to stop, do nothing, and record precisely what it could not decide. An unattended agent that guesses is an unattended agent that acts wrongly at 03:00 with nobody reading.
- Retry becomes reconcile. Interactively, a failed action is retried and the user notices if it double-fires. Unattended, the same retry runs against a world nobody is watching, and the duplicate is found later by an accountant. Every write needs a key and a read-back — see idempotency and retries — and here the key must be stable across firings, not just across attempts within one.
- Reporting becomes rationing. Interactively, more output is helpful. Unattended, output is a claim on someone's attention, and the budget is small and non-renewable.
And the defining property: a scheduled agent fails silently by default. An interactive agent that breaks produces an annoyed user within minutes. A nightly agent that has been erroring since Tuesday produces nothing at all, and "nothing" is exactly what a healthy quiet night also produces. If you build only one thing from this page, build the distinction between those two nothings.
Trigger semantics are a design decision, not a config field.
The scheduler you inherit — cron, a workflow engine, a webhook, a queue consumer — makes four decisions on your behalf that you should be making deliberately.
- Overlap. What happens when firing n+1 arrives and n is still running? For agents the answer is almost always skip, not queue: agent runs are long-tailed, a queue turns one slow run into a thundering herd, and two instances reasoning about the same inbox will both act. Take a named lease with a TTL longer than your task horizon and let the second firing exit immediately, loudly enough to be counted.
- Missed windows. After an outage, does the agent catch up? Default to no. Backfilling eleven hours of skipped firings is how a triage agent posts eleven duplicate comments. Where catch-up is genuinely required, run one firing with a widened time window rather than n firings with the original one.
- Delivery guarantee. Event-driven triggers are usually at-least-once, so the same webhook will arrive twice on a bad day. That is fine if and only if the run is idempotent end to end, which for an agent means the side effects are keyed, not that the reasoning is deterministic — it never is.
- Jitter and alignment. Everything scheduled at the top of the hour hits your model provider at the top of the hour. Spread firings and you convert a rate-limit incident into ordinary traffic; see rate limits and provider capacity.
Poll only what cannot push. A five-minute poll on a mailbox that supports webhooks costs 288 model invocations a day to discover nothing, and the cost is the smaller half — each of those firings is an opportunity for a spurious action. Where you must poll, put a cheap non-model predicate in front of the agent so the expensive loop starts only when something actually changed.
Between firings, the world moved and your notes expired.
An interactive session holds its context. A scheduled agent wakes into a world that changed while it was gone, holding a summary it wrote about a previous state. That summary is the most dangerous input in the system, because it is fluent, plausible, self-authored and stale.
- Read fresh, always. The first act of every firing is to load current state from the system of record, not from the agent's own last output. Treat prior-run notes as a hint about where to look, never as a fact about what is true.
- Carry a watermark, not a narrative. The right hand-off between firings is small and structured: last-processed ID, last-seen timestamp, a set of keys already acted on. A prose "what I did last time" invites the model to reason from its own fiction. Compact structured state is also what makes a firing resumable — see durable state and resumability.
- Expect the environment to have drifted. Tools get renamed, permissions get revoked, a schema gains a field. Interactive agents surface this as a user complaint; scheduled ones absorb it. Stamp the tool-catalog hash on every firing so the change is visible — the discipline in third-party tool drift.
- Bound the accumulation. Any state an agent appends to across firings — a memory file, a running summary, a task list — grows without a user to prune it, and it will quietly consume the context budget until quality degrades for no visible reason. Cap it by size, expire by age, and alert when it stops fitting.
The notification decision is the product.
This is the part teams under-engineer and users judge them on. A scheduled agent's entire interface with a human is the message it chooses to send, and attention is a budget that does not refill. Send one notification too many for two weeks and the channel is muted; from that point the agent is running and no longer functioning, and nothing in your monitoring will say so.
Make the send decision an explicit, testable step rather than an emergent property of the prompt:
- Every firing ends in a verdict, not a report. Three values are enough:
acted,no-op,blocked. The verdict is a structured field the harness records; the prose is optional and attached to it. - Notify on transitions, not on states. "Still healthy" is not news. "Healthy after three failed nights" is. Diff against the previous verdict and send when it changes, which also gives you a natural recovery notification for free.
- Collapse consecutive no-ops. A run of quiet firings is one line, not forty. Keep them in the log at full fidelity and out of the notification.
- Always notify on
blocked. An agent that stopped because it could not decide is the case with a human decision waiting behind it, and it is the one most often lost in a digest. - Lead with what the reader would act on. The first sentence is the whole message for most recipients; put the finding there, not the fact that the job ran.
- Rate-limit the channel itself, independently of the agent's judgement. A cap of k notifications per day, with the overflow rolled into a digest, is the control that survives an agent bug — because the failure you are defending against is precisely an agent that has decided everything is urgent.
The mirror of this is async agent UX from the design side: the same problem, stated as what the recipient experiences.
Blast radius, when nobody is watching the meter.
Unattended execution removes the human who notices that something has been running for an hour. Two costs run away in the dark, and both need limits enforced by the runtime rather than requested in the prompt.
Spend. A per-firing step budget and token cap, plus a rolling daily cap across all firings of the same schedule. The per-firing cap alone is insufficient: the classic incident is a trigger that starts firing far more often than intended, where every individual run is within budget. Both limits belong in the harness — the reasoning in cost control in the loop, with the added point that here there is nobody to hit stop.
Effect. Cap the number of external write actions per firing at a number you chose deliberately, and make exceeding it a blocked verdict rather than a truncation. An agent that files three tickets is working; an agent that files three hundred was going to file three thousand. This limit catches loop pathologies that no amount of prompt care prevents.
Then make sure a human can actually stop it. A kill switch for a scheduled agent must be checked at the start of every firing against a server-side value — a flag in the deployed configuration is not a kill switch if the next firing needs a deploy to see it. And disabling the schedule and disabling the agent are different operations: teams routinely pause the cron entry while webhook triggers keep arriving.
Watch for absence, not just for errors.
Standard monitoring alarms on bad events. The characteristic failure here is the absence of events, so the monitoring has to be inverted.
- Alert on the missing firing. A dead-man's switch per schedule: if no run has been recorded within one-and-a-half intervals, page. This single alarm catches the whole class of failure where the scheduler, the credential or the container quietly stopped, and almost nobody has it on day one.
- Give every firing a trace and a verdict, including the no-ops. A no-op with a trace is evidence of health; a no-op with no record is indistinguishable from not running. See tracing and observability.
- Track the no-op ratio as a quality metric. A schedule that acts on 2% of firings is mostly burning money and may want a cheaper trigger; one that acts on 100% is probably under-scoped and is doing work someone will eventually dispute. Watch the ratio move, not its absolute value.
- Keep a registry of schedules with an owner and an expiry. Scheduled agents accumulate, outlive their author and keep spending. Every schedule gets a named owner and a review date, and an unowned schedule gets disabled rather than inherited — the argument in agent inventory and registry, sharpened by the fact that these run without anyone invoking them.
- Replay before you change anything. Because firings are unattended, a prompt or model change lands in production with no user to catch it. Re-run the last hundred firings against the new configuration and diff the verdicts; a change that turns twelve no-ops into twelve actions is telling you something before your users do.
Four controls, in this order. Put a dead-man's switch on every schedule today — the failure you cannot see is the one that is already happening. Make each firing end in a structured verdict of acted / no-op / blocked, notify on transitions between them, and rate-limit the channel independently of the agent's own judgement. Give the harness a hard cap on external write actions per firing and a rolling daily spend cap across firings, both enforced outside the prompt. And make abstention the defined behaviour for every ambiguity: an unattended agent that stops and says why is working correctly, and one that guesses is not, however good the guess turns out to be.
Related: concurrency and scaling for what happens when many schedules fire at once, incident response for agents for the run that went wrong overnight, background coding agents for the best-developed instance of this pattern, and human in the loop for where the person goes when there is not one in the room.