Skip to main content
This guide composes Clawboo’s scheduler into a real workflow: you’ll create a Routine that fires a team task on a clock, understand why a failing Routine parks itself instead of retrying, and pause, resume, run, or delete it from one surface. Use it when you want a nightly report, a morning briefing, or a one-off future run to happen without anyone sitting at the dashboard to kick it off. A Routine is a cron-shaped trigger that, on each fire, materializes a task on the board and dispatches it through the ordinary executor pipeline; budgets, approvals, verification, observability, and (for file-mutating work) a worktree all apply exactly as they would to a hand-created task. There is no privileged “scheduled” path. For the full model behind the durable ledger and the rebuildable ticker, read Scheduling; for the UI controls, the Scheduler tab; for the request/response shapes and status codes, the Schedules API. This page is the task-oriented composition of those three.

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

  1. Click Schedule to open the create dialog.
  2. Pick an Agent. Each option shows the agent name and its runtime.
  3. 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”.
  4. Pick when it Runs from the cron presets (see Choose a cadence below).
  5. Give it a Label (optional; defaults to “Scheduled task” for a team task).
  6. Click Create schedule.
On success the dialog closes (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.
The full request/response shape, every field, and every status code live in the Schedules API reference.

Choose a cadence

A Routine’s cronSpec 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:
After a one-shot fires successfully it re-enters 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-manual owner already fires is refused with a 409 (code: "duplicate_firing_owner"). This is a data refusal; never retry it. The guard is domain-scoped: it reads only tasks.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 in error forever. Binding a recurring spec is refused at registration with a 400 (code: "bound_recurring_schedule"); use a once@<iso> spec to bind, or leave teamTaskId unset for a recurring Routine that mints fresh tasks.
See Scheduling → The one-firing-owner invariant for the three walls that enforce this (registration de-dup, the atomic claim, the Gateway source’s refusal) and the board for the atomic-claim mechanism.

What happens when it fires

When a Routine is due, the ticker flips it to queued, 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).
Each fire emits a sequence of observability events under the run’s trace (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 to error, 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; a managed 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

A PATCH 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 nextRunAt countdown (in 5m, in 1h, …). The panel re-fetches GET /api/schedules every 8 seconds, so the countdown and status stay live.
  • When a fire is due, the status pill flips queued → claimed → running, then back to idle (re-armed) on success. The row then shows ran <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 error pill with a lastError line and an empty next-run countdown; fix the cause and Resume to re-arm.

Troubleshooting

A create returns 409. You bound the Routine (via teamTaskId) to a board task another non-manual owner already fires, the one-firing-owner refusal. This is a conflict, not a transient error; do not retry. Bind to a different task, or leave teamTaskId unset so each fire mints its own task.
A create returns 400 with code: "bound_recurring_schedule". You bound a recurring spec to an existing team task. A bound task is claimable once, so a recurring fire would park forever. Use a once@<iso> spec to bind, or drop the teamTaskId for a recurring Routine.
My recurring Routine stopped firing on its own. It hit the error-halts policy; a fire failed, the row parked in error, and nextRunAt is null. Check the lastError on the row (and the run’s obs trace), fix the cause, then Resume. Clawboo will not silently retry a broken fire.
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

Last modified on June 26, 2026