Prerequisites
The Scheduler tab is always available; open it from the Scheduler nav item. Everything here also works directly against the
/api/schedules REST surface if you prefer the API.- At least one agent exists. The create dialog populates its agent picker from
GET /api/agents, and a Routine targets one agent. - A team task Routine works for any runtime class: native, the wrapped one-shot runtimes (Claude Code, Codex, Hermes), or OpenClaw. Routines are the single external wake for all of them, so a mixed-runtime team has one scheduling surface regardless of what each runtime can do on its own. If a runtime isn’t connected yet, see Connecting runtimes.
- Scheduling an OpenClaw agent’s own life (a Gateway cron job, the other domain; see Two cron domains) additionally needs the OpenClaw Gateway connected and this device paired, because that write rides the operator connection. Team-task Routines have no such dependency.
Two cron domains: team work vs. a runtime’s own life
The Scheduler tab shows two kinds of schedule side by side and never conflates them. Knowing which one you want is the first decision.
This guide is about the
team-task domain, Routines. The runtime-own-life domain is an operator surface over schedules the Gateway owns; Clawboo reads and writes them through the Gateway but never fires a team task into them. The separation is enforced structurally: a team-task create aimed at the Gateway-cron source is refused with a 422 domain violation. See Scheduling → The two cron domains for the rationale.
Create a Routine
From the Scheduler tab
- Click Schedule to open the create dialog.
- Pick an Agent. Each option shows the agent name and its runtime.
- Choose the Schedule intent: A team task (a Routine, available for every agent). The other chip, Its own life, is enabled only when the selected agent’s runtime is
openclaw; for any other runtime it reads “OpenClaw only” and is disabled. Leave it on “A team task”. - Pick when it Runs from the cron presets (see Choose a cadence below).
- Give it a Label (optional; defaults to “Scheduled task” for a team task).
- Click Create schedule.
201) and the list refreshes. Under the hood the panel posts { source: 'clawboo-routine', domain: 'team-task', agentId, cronSpec, label, teamId, taskTemplate }.
From the API
The same create over REST.source, domain, agentId, and cronSpec are required; everything else is optional. The taskTemplate describes the board task each fire materializes; title is required, kind defaults to code (which provisions a worktree), and you can thread a per-node cost cap with maxNodeCents.
Choose a cadence
A Routine’scronSpec is one of two shapes.
Recurring: a cron expression
The create dialog offers eight cron-expression presets. A cron expression is the one spec dialect both schedule sources accept, so the same preset works for a Routine or a Gateway cron. The dialog defaults to Every hour.
Any croner-parseable 5- or 6-field cron expression works, not just the presets; post your own
cronSpec to the API if you need a different cadence (a 6th field adds seconds). An unparseable spec is refused at creation with a 400 (code: "invalid_cron_spec").
One-shot: once@<ISO-8601>
A Routine also accepts a one-shot form, once@<ISO-8601> (for example once@2026-07-01T09:00:00Z), for a single run at a future time. The dialog’s preset chips only emit recurring expressions, so a one-shot is created via the API:
idle with nextRunAt set to null; it self-disables and never repeats. A malformed once@ timestamp is also a 400.
The one-firing-owner invariant
A board task must have exactly one scheduler. Two schedulers firing the same task is the recipe for double-dispatch, stale claims, and drift, so Clawboo enforces a single firing owner of record on every task (tasks.scheduled_by: manual for a hand-created task, clawboo for one a Routine fires).
For most Routines this is invisible; each fire mints a fresh per-fire board task stamped scheduled_by: 'clawboo', and nothing collides. The invariant only bites when you bind a Routine to an existing team task by passing a teamTaskId in the template, telling the Routine to dispatch that one task rather than a new one each fire. Two rules apply:
- A bound task can have only one firing owner. Binding to a task that some other non-
manualowner already fires is refused with a409(code: "duplicate_firing_owner"). This is a data refusal; never retry it. The guard is domain-scoped: it reads onlytasks.scheduled_by, so a runtime’s own-life cron never trips it. - A bound Routine must be one-shot. A bound task is claimable exactly once (
todo → done), so a recurring schedule against it would fire once and then park inerrorforever. Binding a recurring spec is refused at registration with a400(code: "bound_recurring_schedule"); use aonce@<iso>spec to bind, or leaveteamTaskIdunset for a recurring Routine that mints fresh tasks.
What happens when it fires
When a Routine is due, the ticker flips it toqueued, atomically claims it, materializes the board task, and branches on the target runtime’s integration class, never on a hardcoded runtime id:
- Native, Claude Code, Codex, Hermes run through the ordinary one-shot executor: claim the board task, provision a worktree if the kind requires it, run the adapter, verify, complete.
- OpenClaw runs over its live Gateway connection through a separate operator dispatcher, bounded by a watchdog (10 minutes by default, overridable with
CLAWBOO_ROUTINE_OPENCLAW_TIMEOUT_MS).
routine_fired, routine_dispatched, then routine_completed or routine_error), so you can follow a scheduled run in the Observability dashboard exactly like any other task. The full fire path is in Scheduling → The fire path.
The error-halts policy
When a recurring fire fails, the Routine parks itself: status goes toerror, the failure is recorded in lastError, and nextRunAt is set to null, disarmed. It will not fire again until a human resumes it.
This is deliberate, and it’s the single most important behavior to internalize. Autonomous scheduled work that retries a broken fire on every tick would burn budget, churn the board, and bury the real problem. Parking surfaces the failure and stops the bleeding. A successful fire, by contrast, re-arms cleanly at its next occurrence, and a one-shot self-disables.
A parked (
error) Routine and a paused Routine both never auto-fire; the ticker’s due-pass only ever queues idle rows. To bring a parked Routine back, fix the underlying cause and Resume it (the error → idle transition re-arms it). A once@ that fired successfully is not an error; it self-disabled on purpose.A failed dispatch that is really a lost claim (some other worker already owns the task, so the work is happening) is recorded as satisfied, not as an error; it does not park the Routine.
Manage a running Routine
All three controls are a pure function of the schedule’s manageability tier; amanaged Routine is fully writable.
Pause and resume
Click the pause/play button on the row (PATCH /api/schedules/:id with { action: 'pause' | 'resume' }). A paused Routine never auto-fires until you resume it. Resume re-arms it to idle with a freshly computed nextRunAt. An illegal pause/resume from the row’s current status returns a 409.
Run now
Click the refresh-arrow button to force-fire immediately (POST /api/schedules/:id/run). This returns 202, an enqueue-style acknowledgement, not a synchronous run. For a Routine it flips the row to queued so the ticker picks it up on the next pass; it does not wait for the run to finish. Watch the trace in the Observability dashboard to see the outcome.
Change the cadence
APATCH with a patch object updates the cron spec, label, or task template in place. Changing the cron spec recomputes nextRunAt only for an already-armed (idle) row; a paused or parked row stays disarmed until you resume it.
Delete
Click the trash button (DELETE /api/schedules/:id) to remove the Routine permanently. The panel confirms first.
Verify it worked
- The new Routine appears under the Team work group with a live
nextRunAtcountdown (in 5m,in 1h, …). The panel re-fetchesGET /api/schedulesevery 8 seconds, so the countdown and status stay live. - When a fire is due, the status pill flips
queued → claimed → running, then back toidle(re-armed) on success. The row then showsran <relative time>. - A fire materializes a task on the board for the Routine’s team; open the board to see it.
- The run shows up as a trace in the Observability dashboard, tagged with the
routine_*events. - If a recurring fire fails, the row goes to the
errorpill with alastErrorline and an empty next-run countdown; fix the cause and Resume to re-arm.
Troubleshooting
Restarting the server doesn’t lose my Routines. The ticker holds no durable state; the
scheduled_runs ledger is the source of truth, and boot-resume reconstructs every active Routine from SQLite. A claimed orphan re-fires; a recurring running orphan re-arms; a one-shot running orphan parks in error for a human to inspect (its outcome is unknown). See Scheduling → Boot-resume.See also
- Scheduling, the model: the two cron domains, the ledger, the rebuildable ticker, the fire path
- The Scheduler tab, every panel control and the create dialog in detail
- Schedules API, full request/response shapes and status codes
- The board, where a fire lands, the atomic claim, the firing-owner column
- Connecting runtimes, get a runtime online so it can run scheduled work
- Cross-runtime handoff, another way scheduled work composes across runtimes
- Governance and budgets, cap what a scheduled fire can spend
- Observability dashboard, watch a scheduled run’s trace
- Glossary, canonical term definitions