Skip to main content
The goal: at 06:00 Pacific every weekday, an agent reads yesterday’s errors, writes a summary, and files it as a deliverable. Nothing external calls it.

1. Create it with the schedule attached

No completion_window here, on purpose. standard and flex are served only on zai-org/GLM-5.2-FP8; on anthropic/claude-sonnet-4-5 a non-asap window is meant to be refused at the first model call with window_unavailable rather than downgraded, which would turn this agent’s 6am wake into a failed task. 🔴 On deployed staging the refusal did not fire — the wake ran and was billed on asap under a flex request — so the downside is a silent wrong tariff rather than a loud failure, which is worse to notice. If the agent is pinned to GLM-5.2, completion_window: "flex" is the right setting for work nobody is waiting on — see pricing.
🔴 agent.next_wake_at does not tell you the alarm is armed. It, along with status, wakes, attention and blocked_reason, is a Postgres mirror that nothing writes back to: the runtime holds the alarm and never reports it, so the field reads null on a scheduled agent that is about to fire. Confirm a schedule by looking at the board for tasks with source: "schedule", as in step 3.
Five cron fields — min hour dom mon dow. Seconds are not a field, and a 6-field expression is refused at create with invalid_input. tz is IANA and defaults to UTC; use a real timezone if you care about daylight saving, because 0 6 * * * in UTC drifts an hour against Pacific twice a year.
Or from the CLI, where --cron takes "<5-field cron>=<the work>":

2. Several schedules on one agent

schedule takes an array.
enabled: false keeps the row without arming it — the way to pause one schedule without losing its definition.
A schedule entry’s window is accepted, stored, echoed back on GET, and dropped. The value is validated at the edge and again when the config reaches the runtime, and then the runtime’s schedule table has no column for it: every schedule-fired task runs asap. Set completion_window on the agent if you want a different window on its scheduled work.

3. Check that it actually woke

The board records which door the work came through, so a schedule-fired task is distinguishable from one you sent by hand:
Collapsed fires are real; missed_fires does not report them. If the agent was still busy when the next fire landed, the fires are collapsed into one task rather than queued into a backlog that can never drain — and the collapsed count is written into the task’s own text (“This schedule fired N times while you were unavailable… You are late.”). The missed_fires field on a task record is hard-coded to 0, because the runtime counts collapses against the schedule and not against a task. Read the task text, or the thin board, not that field.

4. Read what it delivered

Small text artifacts come back inline as content. Larger ones come back as a download_url minted fresh on this read, valid for an hour and unauthenticated once issued — do not cache it, call again.

5. Stop it without deleting it

🔴 paused does not stop anything yet. The flag is stored on the agent row and returned by get, and it is not one of the keys pushed to the runtime — the durable object never sees it, so a paused agent keeps firing its schedule. To actually stop a schedule, remove or disable its entries, which are pushed and reconciled on every wake:
Neither aborts a slice that is already running — use cancel for that, and remember it is cooperative.

6. What it costs to leave running

Between 06:00 and 06:00 the next day this agent is asleep with one alarm set for its next fire, and it costs storage. You are billed for the wake, the model calls inside it, and any paid tools it used.
There is no threshold notification to subscribe to — alert_at is stored and nothing fires off it. Read the budget event on the stream instead: it carries spend against the cap on every turn, before the call it is gating.

Failure modes worth wiring up

Every diagnosis below reads the board, never agent.status. The agent record’s status, blocked_reason, attention, next_wake_at and wakes are a mirror nothing writes back to; a braked or blocked agent still reads status: "idle".
A task with status: "braked" on the board means the cap bound. It does not restart on its own — the board never claims a braked task, and neither the period reset nor a higher cap moves it back. Raise the cap and re-send the work:
budget on update is a full replacement, so send every field you want kept.
A task sitting at status: "waiting" on the board. Find it and answer it:
A scheduled agent that parks at 6am and is not answered stays parked. If you do not want it asking, give it mode guidance in instructions — a task sent with mode: "ask" gets read-only tools, but a schedule’s work always runs as act.
Check the primitive first, then the agent’s own log:
An empty board with source: schedule and an empty log is the real signal. Do not read next_wake_at for this: it is null on every agent, armed or not. The usual causes are every schedule entry having enabled: false, or a cron the agent’s parser rejected at config push — which surfaces as agent_not_configured on the next task rather than as a silent no-op.