There is a specific kind of disappointment that comes from scheduled agents, and it always arrives the same way. You set one up. It works when you test it. Three weeks later you notice it hasn't done anything in eleven days, and there's nowhere to look to find out why.
We shipped Routines this week — a standing instruction with a trigger attached. Pick an agent, pick when, and it wakes a computer in the cloud, does the work, and tells you what came of it. Nothing of yours has to be switched on.
That description fits about a dozen products. What follows is the part that doesn't.
The four ways this goes wrong #
Before building anything we went looking for what people actually hit with scheduled agents. The complaints cluster hard.
It didn't run and nobody said so. This is the top one, and it's a category error in the design rather than a bug: the config file says what should run and has no opinion about what is running. Claude Cowork's scheduler has an open issue where tasks skip on a timezone offset and simply don't appear. Nothing failed loudly. Nothing appeared at all.
It ran, spent money, produced nothing. "Silent failure" is its own recurring complaint in agent communities — no crash, no signal, no value, just a charge. Telerik has a whole taxonomy of the ways an agent returns status: ok
having done nothing useful.
Cost runs away. Context accumulation compounds on every scheduled run. An agent loop costs roughly 3.2× a chatbot turn at five steps and over 30× at fifty, and a polling schedule pays full price on every run where nothing happened.
Time is the only trigger. If something breaks at 15:00 and your check runs at 23:00, you find out at 23:00.
Every one of those became a constraint. Here's what came out.
A routine is a sentence #
Scheduling is the single thing in this product a non-technical person is most likely to want, and a cron field is a literacy test. So the form is a sentence you complete, and the expression is generated underneath.
Every weekday at eight. Every Monday at nine. Every day at two in the morning. If you already think in cron, there's a custom option that takes the raw expression and tells you back, in words, what it thinks you meant — because the failure mode of a cron field is not rejection, it's silently scheduling something else.
Times are stored with an IANA timezone, never an offset, and the next fire is recomputed after each run rather than incremented. That is the whole reason daylight saving doesn't drift: a local time that doesn't exist in spring fires at the next real instant, and a local time that happens twice in autumn fires once, on the first. Both are pinned by tests with fixed clocks, because this is precisely where the competition breaks.
Pick who does it, not just when #
A routine runs as one of the specialist agents, the same roster your tasks use.
"Research this every Monday" and "sweep this repo every night" are not the same job and shouldn't get the same worker. If you don't want to choose, Assistant works out who you needed.
Every non-run is a row #
This is the design decision the rest hangs off, and it's the one we'd defend hardest.
A fire request that gets refused — because the routine was off, the account was out of budget, the machine was in cold storage, the previous run was still going — is not dropped. It becomes a run row with a status that says which. The queue is the audit trail of everything that wanted to happen, not just everything that succeeded.
So the history reads in plain sentences. Done. Nothing to report. Didn't run — machine was in cold storage. Hit its cost cap. A status that renders as a blank cell is the silent-skip bug wearing our own colours, so every status we can write has a sentence, and the fallback still says something specific.
Each row also carries what it cost and how long it took, and the header totals them: 7 runs · $0.63 · typically 3m 8s. Those are the two questions a standing instruction raises once it's been running a while, and the two you cannot answer from a list of dates.
The list view compresses the same thing into a heartbeat — one tick per recent run, coloured by outcome.
You can see at a glance which routines are healthy and which one has been failing without telling you. The last one there turned itself off: after five consecutive failures, a routine disables itself and tells you, because a broken routine must not bill forever.
Not spending your money on nothing #
Three things, in order of how much they save.
A fresh conversation each run, by default. Context accumulation is the dominant driver of scheduled-agent cost runaway — a continued thread pays for every previous run's context, forever. There's a checkbox to keep one long conversation when the thread genuinely is the value, and it's labelled honestly: keeps context between runs, costs more over time.
An optional check command that costs zero tokens. A shell command runs on the machine before the agent does. Exit non-zero and the run ends as "nothing to do" without a single model call. git fetch && git log --oneline @..@{u} | grep .
is a complete, honest change detector, and you can read it. We deliberately did not make this a natural-language condition: an LLM condition costs a model call to decide whether to make model calls.
A per-run cost cap. Crossing it ends the run, keeps what was spent, and shows it — that's the "Hit its cost cap" row above.
There's also a minimum interval per plan, which is the honest version of a rate limit: every run wakes a real computer, and that costs real compute whether or not the model does anything.
State is a file, not a vendor black box #
Every routine gets ~/routines/<slug>/
on its machine, and the seeded convention is: read state.md
before you start, write what the next run needs to know before you finish.
This is the part we get for free and the competition doesn't. Every other product's answer for memory between runs is "externalize it" — write a learnings.md
into some vendor store. We don't need the workaround, because a routine already has a persistent disk on a real machine. You can open the file. In Files, in the terminal, in the next run. Edit it to correct the routine's memory, delete it to reset.
Run it before you schedule it #
The form has Run it now next to Save, and it fires the real path — the same queue, the same runner, the same billing lane as a scheduled fire. Not a preview mode.
A routine you have never watched run is a routine you cannot trust, and the failure you want to catch is not "it errored", it's "it ran fine and reported something useless". You only find that out by reading one.
Waking the machine is the point #
Nearly every fire lands on a sleeping box — free machines suspend after 5 idle minutes, Pro after 15 — and starts by waking it. That's not a side effect to be engineered around; it's the promise. Your computer sleeps, costs nothing, and gets up when the routine says so.
Two consequences we had to handle rather than hide. The wake goes through the same admission door as pressing Start, so it can be refused — and a refusal is a visible skip with its reason, never a silent no-op and never an overdraft. And a machine with an enabled routine is exempt from cold storage, because teaching the runner to wait several minutes for a restore is worse than just not archiving it.
This is not the agent's own /schedule #
Worth drawing this line hard, because both things exist, both are called scheduling, and confusing them wastes an afternoon.
Octomind already ships a schedule
tool and a /schedule
command, and the agent reaches for it while it's working: remind me in twenty minutes, check this build again in an hour. Those entries live in that session's own process, fire into that conversation, and die with it. It's a good primitive with a deliberately narrow scope — an agent setting a timer for itself, from inside a conversation that's already running.
A routine points the other way. It fires from outside the machine entirely, and the first thing it does is start the machine.
That difference is structural, not a preference. Your box is asleep most of the time — free machines suspend after five idle minutes, Pro after fifteen — and a suspended container runs no timers. A schedule living inside the box cannot wake the box: by the time it should have fired, the process holding it no longer exists. Anything that has to survive the machine being off has to be triggered from the outside, which is where routines live and /schedule
structurally cannot.
So /schedule
is the agent organising its own work within a session it's already having. A routine is the thing that creates the session in the first place. Both keep their names, and you'll want both.
Time is one trigger, not the architecture #
Under the hood, nothing in the runner knows what caused a run. Every trigger source does exactly one thing: decide a routine should run, and write a fire request to a queue. Admission, wake, check, session, turn, record, deliver — none of it ever learns what asked.
PRODUCERS (many, pluggable) QUEUE CONSUMER (one)
timer due-scan on next_fire_at ┐
webhook an inbound POST ├─► routine_fires ─► RoutineRunner
connector a message / PR / issue ┤ (dedup, claim)
workflow a run finished ┘
Today the timer producer ships, and so does the manual one — Run it now writes a fire request like anything else, which is exactly why the test button exercises the real path instead of a parallel one. Webhook, connector and workflow triggers land without touching the runner, the guards, the billing lane or the history, because none of them read the source except to print it.
That's the test for whether this seam is right: adding a trigger must not require a migration, a runner change, or a new concept in the UI. If a proposed source fails that test, the seam is wrong.
What it tells you, and where #
A run nobody sees didn't happen. Results ring the panel bell, and if you've connected Telegram, Slack or WhatsApp, the existing notification mirror carries them into that chat — which is where routines get genuinely useful, because the morning brief arrives where you already are.
The default is to notify on change, which means the agent must say "nothing changed" — so the starter prompts ask for exactly that sentence, and a zero-token skip from the check command counts as no-change without a model call. Failures always ring, whatever the setting. Silence is the bug we're building against.
Limits #
Free gets one routine, once a day. Pro gets ten, down to every fifteen minutes. Max and Team get thirty, down to every five.
Webhook triggers aren't built yet — the queue and the source seam are, which is the part that's expensive to retrofit. Timeouts and cost caps end our turn rather than killing the agent mid-thought, which is a real limit we'd rather name than paper over.
Open Routines and start with one. Watch it run once before you schedule it — that's the whole advice.