team-task domain, fully managed) and the OpenClaw Gateway cron (the runtime-own-life domain, written through the Gateway’s own operator RPC). A read always succeeds and reports per-source degradation as data; a write routes to the owning source by id and surfaces the typed scheduling errors as precise status codes.
The two sources are never conflated. A Routine row schedules team work on a cadence, for any runtime class: a message to a team’s lead (a team routine) or a board task for one agent (an agent routine). A Gateway-cron row schedules an OpenClaw agent’s own standalone life. clawboo never registers team work into the Gateway cron, and never auto-creates own-life crons; the Routines view is an operator surface over them, not their owner.
id of the form <source>:<rawId> (clawboo-routine:<ledger-row-id> or openclaw-gateway-cron:<gateway-job-id>). The :id path segment on the mutation routes is URL-decoded and parsed back to its owning source; the write routes there. An id that matches no source returns 404. All POST/PATCH bodies are parsed by express.json({ limit: '2mb' }).
Routes
The ScheduleRecord shape
Every source projects its rows into one normalized record. This is the element type of the schedules[] array on GET, and the value returned (under schedule) on a successful create/update/pause/resume.
There is no third source. Claude Code, Codex, Hermes, and clawboo-native have no live native scheduler; scheduling any of them is a clawboo Routine.
GET /api/schedules
The merged view. Fans read() across both sources and concatenates their records, then resolves each record’s teamName and agentName. A team routine has no stored agent, so it reports the team’s current lead (the agent its next fire would reach) as agentName and runtime. A source that fails or is disconnected does not fail the request; it contributes a degraded sources[] entry instead (a warm Gateway-cron cache is served stale; otherwise its rows are simply absent until reconnect). This route always returns 200.
- Path/query params: none.
- Request body: none.
Responses
200 OK: the merged records plus a per-source status array:
{ ok: true, degraded: false }. The Gateway-cron source reports { ok: false, degraded: true, reason: 'gateway_disconnected' } (no cache) or reason: 'stale_cache' (warm cache) when its operator connection is down.
Example
POST /api/schedules
Creates a schedule. The body is a ScheduleCreateSpec; the multiplexer routes the write to spec.source. Before the source is touched it enforces, in order: an observe-only source rejects with 403, and a team-task create aimed at a runtime-own-life source rejects with 422 (defense-in-depth; the Gateway-cron source refuses it too). The owning source then performs its own validation.
For a clawboo-routine create: the cron spec is probed (an unparseable spec throws), a task template is built from label + taskTemplate + target (the template’s title defaults to the label, and its description is the instructions each fire sends), and the target is validated against the registry:
- A team routine (
target: 'team') needs ateamIdnaming a live, unarchived team. Its fires are posted into that team’s chat for the team’s lead, so the row stores no agent (agentIdis ignored and reads back as''), and it cannot bind ateamTaskId. - An agent routine (
target: 'agent', the default) needs anagentIdnaming a live, unarchived agent, and is filed on that agent’s own team. AteamIdis optional; when given it must match the agent’s team (nullfor an agent on no team).
code: "invalid_routine_target". A recurring spec bound to an existing teamTaskId is refused too (a bound task is claimable exactly once, so a recurring fire would park in error forever; bind only one-shot once@<iso> specs). Binding to a task already owned by another firing owner is the 409 de-dup refusal.
For an openclaw-gateway-cron create, the source calls cron.add with the job’s payload (default { kind: 'agentTurn', message: label }) and the session target that payload kind requires: main for a systemEvent, isolated for anything else. The Gateway accepts a mismatched pair but then skips every fire, so the pairing is never left to the caller.
- Path/query params: none.
- Request body: a
ScheduleCreateSpec.source,domain, andcronSpecare required, plus ateamIdfor a team routine or anagentIdfor anything else; the rest are optional:
Responses
201 Created: the schedule was registered:
400 Bad Request: the body is missing a required field, has an unknown source/domain/target, or fails a source-side validation. code is invalid_body for the shape check or a zod-rejected template, invalid_cron_spec for an unparseable cron spec, invalid_routine_target for a target that does not check out, or bound_recurring_schedule for a recurring spec bound to an existing task:
403 Forbidden: the target source is observe-only (no live source is observe-only today, but the gate exists):
404 Not Found: spec.source matches no registered source:
409 Conflict: the bound teamTaskId is already scheduled by a different firing owner. This is a data refusal; do not retry:
422 Unprocessable Entity: a domain: 'team-task' create was aimed at a runtime-own-life source (the Gateway cron):
503 Service Unavailable: the target is the Gateway-cron source and its operator connection is down:
500 Internal Server Error: any other throw:
Example
PATCH /api/schedules/:id
Pauses/resumes a schedule, or patches its cron spec, label, task template, payload, or target. The body is one of two shapes; an unrecognized body returns 400. The write routes to the source named in :id.
For a Routine: pause sets the row to paused (disarmed, nextRunAt cleared) and is legal from idle, queued, or error; resume re-arms it to idle with a freshly computed nextRunAt and is legal from paused or error. Neither is legal while a fire is in flight (claimed or running), since that fire settles the row itself. A cron-spec patch recomputes nextRunAt only for an already-armed (idle) row. A patch that sets target, agentId, or teamId re-points the Routine, and the resulting target is validated exactly like a create (a target inside taskTemplate counts as one too). A teamTaskId inside a template patch is ignored: a Routine binds to a board task only at registration, where the firing-owner guard runs. For the Gateway cron: pause/resume map to cron.update { id, patch: { enabled } } (there is no separate enable/disable method), and a patch maps to cron.update { id, patch } with the changed fields (a changed payload carries its matching session target).
- Path params:
id(composite schedule id; URL-decoded; 404 on no-source-match). - Request body: exactly one of:
Responses
200 OK: the schedule was updated; the fresh record is returned (a Gateway-cron update may return schedule: null when the best-effort read-back can’t reload the job):
400 Bad Request: the body is neither a valid action nor a patch object, a patched cronSpec is unparseable, or a re-pointed target does not check out (invalid_routine_target):
403 Forbidden: the target source is observe-only:
404 Not Found: :id matches no source, or the id is unknown within its source:
409 Conflict: the requested pause/resume is illegal from the row’s current status (Routine state machine):
503 Service Unavailable: the Gateway-cron source’s operator connection is down:
500 Internal Server Error: any other throw:
Example
DELETE /api/schedules/:id
Removes a schedule. For a Routine it deletes the ledger row; for the Gateway cron it calls cron.remove. The write routes to the source named in :id.
- Path params:
id(composite schedule id; URL-decoded; 404 on no-source-match). - Request body: none.
Responses
200 OK: the schedule was removed:
403 Forbidden: the target source is observe-only:
404 Not Found: :id matches no source, or the id is unknown within its source:
503 Service Unavailable: the Gateway-cron source’s operator connection is down:
500 Internal Server Error: any other throw:
Example
POST /api/schedules/:id/run
Force-fires a schedule now. This is an enqueue-style acknowledgement, not a synchronous run: a Routine is moved to queued and the ticker, poked by the write, picks it up at once; the Gateway cron is told cron.run { id, mode: 'force' }. Only an idle Routine can be queued: a paused, errored, or already-running one returns 409. Completion is observed elsewhere (the Routine’s run history, the obs event log, or Gateway cron-run polling), not in this response. The write routes to the source named in :id.
- Path params:
id(composite schedule id; URL-decoded; 404 on no-source-match). - Request body: none.
Responses
202 Accepted: the fire was enqueued:
403 Forbidden: the target source is observe-only:
404 Not Found: :id matches no source, or the id is unknown within its source:
409 Conflict: a Routine could not be queued from its current status:
503 Service Unavailable: the Gateway-cron source’s operator connection is down:
500 Internal Server Error: any other throw:
Example
GET /api/schedules/:id/runs
A Routine’s recent fires, newest first, folded from the routine_* events in the obs event log (every one of them carries the Routine’s scheduledRunId). An agent routine’s fire carries the board task it created, resolved to that task’s current title and status. The Gateway keeps its own run history, so a Gateway-cron id returns an empty list.
- Path params:
id(composite schedule id; URL-decoded). - Query params:
limit(optional, default10, clamped to1–50). - Request body: none.
Responses
200 OK:
interrupted when it never recorded an outcome: the next fire began without one, or the Routine is no longer queued, claimed, or running. Both mean the server stopped during that fire. A team fire succeeded once the lead’s turn started; the team’s work then continues in the chat.
404 Not Found: :id is not a composite schedule id, or names no Routine:
500 Internal Server Error: any other throw:
Example
Error envelope
Every error response on these routes is the standard envelope plus a structuralcode: { error: string, code?: string }. The code is a stable, branch-on-able discriminant (never parse the message prose):
A zod-rejected task template returns
{ error: "invalid task template", code: "invalid_body" }.
See also
- Scheduling (Routines): team-task cron vs runtime-own-life cron
- Recurring team work (Routines how-to)
- Routines, the view over this surface
- The board,
teamTaskId, atomic claim, the one-firing-owner guard - @clawboo/scheduler,
ScheduleRecord, the source trait, the multiplexer - System API, OpenClaw Gateway lifecycle (the cron source’s backing connection)
- REST API overview