Skip to main content
REST surface for the unified scheduler: one merged read/write view over two schedule sources: clawboo Routines (the 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 (a board task fired on a cadence, for any runtime class); a Gateway-cron row schedules an OpenClaw agent’s own standalone life. clawboo never registers a team task into the Gateway cron, and never auto-creates own-life crons; the Scheduler tab is an operator surface over them, not their owner.
Every record carries a composite 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.
The two live sources are fixed:
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. 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:
The Routines source reports { 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, and a recurring spec bound to an existing teamTaskId is refused (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.
  • Path/query params: none.
  • Request body: a ScheduleCreateSpec. source, domain, agentId, and cronSpec are required; 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, or fails a source-side validation. code is invalid_body for the shape check, invalid_cron_spec for an unparseable cron spec, bound_recurring_schedule for a recurring spec bound to an existing task, or invalid task template (with no code) for a zod-rejected template:
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, or payload. 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); resume re-arms it to idle with a freshly computed nextRunAt; a cron-spec patch recomputes nextRunAt only for an already-armed (idle) row. For the Gateway cron: pause/resume map to cron.update { id, enabled } (there is no separate enable/disable method), and a patch maps to cron.update with the changed fields.
  • 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, or a patched cronSpec is unparseable:
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 (the ticker picks it up); the Gateway cron is told cron.run { id, mode: 'force' }. Completion is observed elsewhere (the obs event log / 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


Error envelope

Every error response on these routes is the standard envelope plus a structural code: { 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

Last modified on June 26, 2026