Skip to main content
REST surface over the durable board, the transactional source of truth for team/task coordination. These routes create and list tasks, atomically claim a task for a single assignee, transition status through the state machine (with the intrinsic verification gate), record the execution ledger, link dependency chains, cancel a dead downstream chain, and provision / inspect / pause / complete a task’s per-task git worktree (its system-of-record). The board is a thin HTTP layer over @clawboo/db’s board repository, the data boundary. Every POST/PATCH body is validated by a co-located zod schema; an invalid body returns 400 { error: 'invalid body', details: <zod flatten> }. The worktree-handoff route validates against @clawboo/worktrees’ handoff schema and returns 400 { error: 'invalid handoff', details: <zod flatten> } instead. All bodies are parsed by express.json({ limit: '2mb' }).
The atomic claim is the board’s concurrency primitive. POST /api/board/:taskId/claim returns 409 when another worker already won. Per the board contract, a 409 is data, not a transient error; do not retry it. The loser did not lose a race it can re-run; the task is simply taken. The same rule applies to the 409 illegal_transition on PATCH /api/board/:taskId.

Routes

The worktree routes share a :taskId prefix; the router registers the longer two-segment paths (/workspace/handoff, /workspace/detail) before the bare /workspace, and the execution paths (/executions, /executions/:execId) are distinct two-segment forms, so there is no path collision with /:taskId.
The 7 task statuses are backlog, todo, in_progress, in_review, blocked, done, cancelled. done and cancelled are terminal. The legal forward transitions are fixed by the state machine; an illegal transition is rejected with 409 illegal_transition.

GET /api/board

Lists tasks for a team, or the subset that is ready to work. With ready=true the response is getReadyTasks (status todo, not dropped, with every dependency done); otherwise it is listTasks filtered by teamId / status / includeDropped. Dropped (soft-deleted) tasks are excluded unless includeDropped=true.
  • Query params:
  • Request body: none.

Responses

200 OK: the task list (tasks[] is DbTask[], newest-updatedAt first; the ready variant orders by priority then updatedAt):
500 Internal Server Error: any DB failure:

Example


POST /api/board

Creates a task. status defaults to todo (immediately claimable); pass backlog for triage. A subtask is created by setting parentTaskId (the parent chain bounds delegation depth). This route is not capped; the per-parent child-count and depth ceilings are enforced at the Tasks MCP boundary, where an attached model creates rows unsupervised. On success the handler emits a task_created observability event (best-effort: an append failure is swallowed and never fails the request).
  • Path/query params: none.
  • Request body (validated by createTaskBody):

Responses

400 Bad Request: body failed validation:
200 OK: the created task (full DbTask, shape as in GET /api/board):
500 Internal Server Error: DB failure:

Example


GET /api/board/:taskId

Returns one task plus its comments (oldest first) and its ancestor chain (the parent-task lineage, via recursive CTE).
  • Path params: taskId.
  • Request body: none.

Responses

200 OK:
404 Not Found: no such task:
500 Internal Server Error:

Example


POST /api/board/:taskId/claim

Atomically claims a todo task for a single assignee. The claim is a guarded UPDATE (status='todo' AND assignee IS NULL AND dropped=0); at most one caller wins, the task flips to in_progress, and the loser gets a 409. On a win the handler emits a task_claimed event and narrates the claim into the team chat room (best-effort, after the canonical write, never a write path back).
A 409 means another worker holds the task. Do not retry it. A dead in_progress task is recovered by orphan reconciliation (which releases it to todo), after which a normal claim re-acquires it; the liveness logic lives there, not in the claim.
  • Path params: taskId.
  • Request body (validated by claimBody):

Responses

400 Bad Request: body failed validation:
200 OK: the claim won; the task is now in_progress and assigned:
404 Not Found: the task does not exist (reason: 'not_found'):
409 Conflict: the task was not in a claimable state (already claimed, dropped, or not todo):
500 Internal Server Error:

Example


PATCH /api/board/:taskId

Transitions a task’s status and/or edits its metadata (priority / title / description). At least one field is required. The status change is enforced by the state machine inside a BEGIN IMMEDIATE transaction (illegal transition → 409). When the target is done, the intrinsic verification gate applies: a task carrying a non-promotable verdict (a failing deterministic gate, including red-gate debt) is rejected with 409 verification_required. The only bypass is humanOverride: true; when used with status: 'done' the override is recorded in the governance audit log so it is never silent.
A task with no stored verification verdict is unverified, not failing; it lands done normally. The gate blocks known-failing verdicts, not un-run verification; manually completing unverified work is an intentional human judgment call (the autonomous path always writes a verdict via the verification gate before this transition). Moving a task to todo releases it (clears the assignee + verdict) so the atomic claim can re-acquire it.
On a successful status change the handler emits a status_changed event and narrates the mutation into the team chat room (best-effort, after the canonical write).
  • Path params: taskId.
  • Request body (validated by updateTaskBody; at least one field required):

Responses

400 Bad Request: empty body or failed validation:
200 OK: applied; the updated task:
404 Not Found: the task does not exist (reason: 'not_found', or the field update found no row):
(or, when only fields were edited and the row vanished:)
409 Conflict: the status transition is illegal or the verification gate blocked →done:
500 Internal Server Error:

Example


POST /api/board/:taskId/comments

Adds a comment to a task (discussion or a system note). authorType defaults to agent. The handler emits a comment_added event and narrates a truncated form of the comment into the team chat room (best-effort, after the write). The handler does not 404 a missing task before inserting; the comment row is created against the supplied taskId.
  • Path params: taskId.
  • Request body (validated by commentBody):

Responses

400 Bad Request: body failed validation:
200 OK: the created comment:
500 Internal Server Error:

Example


POST /api/board/:taskId/executions

Opens an execution-process row for a task, recorded only after a successful claim. The exec ledger is what orphan reconciliation reads on restart: an executor that starts work MUST open one here and close it via the PATCH below, or a crash leaves the run orphaned (reconciliation then marks it failed and releases the task). On success the handler emits an execution_started event.
  • Path params: taskId.
  • Request body (validated by createExecutionBody):

Responses

400 Bad Request: body failed validation:
404 Not Found: the task does not exist:
200 OK: the created execution row (starts in status: 'running'):
500 Internal Server Error:

Example


PATCH /api/board/executions/:execId

Closes out an execution row with its outcome and an optional token/cost ledger. The handler emits an execution_completed event carrying the run’s taskId, teamId, agentId and runtime, recovered from the closed row, so the event correlates with the execution_started it pairs with. A terminal ledger row is immutable, so only a still-running execution can be closed: an execId that is unknown, or whose row some other path already closed, closes nothing and returns 404 without appending an event.
  • Path params: execId.
  • Request body (validated by completeExecutionBody):

Responses

400 Bad Request: body failed validation:
200 OK: the execution was closed:
404 Not Found: no still-running execution row matched execId, either because the id is unknown or because the row is already terminal (most often the stale sweep closed it as timed_out). Nothing was closed and no event was appended, and this 404 is data rather than a transient error to retry:
500 Internal Server Error:

Example


GET /api/board/:taskId/executions

Lists a task’s execution-process rows (the run ledger), every spawned run, oldest first.
  • Path params: taskId.
  • Request body: none.

Responses

200 OK: the ledger (executions[] is DbExecutionProcess[], shape as in POST .../executions):
500 Internal Server Error:

Example


POST /api/board/:taskId/deps

Links a dependency: taskId will not become ready until dependsOnTaskId is done. Plans become a dep chain; the orchestrator’s ready-pump fires the next step when its blocker completes. Both endpoints of the edge must exist (the handler guards against orphan dep rows). On success the handler emits a dep_linked event and narrates the dependency into the team chat room.
  • Path params: taskId (the dependent).
  • Request body (validated by linkDepBody):

Responses

400 Bad Request: body failed validation:
404 Not Found: either the task or the blocker does not exist:
200 OK: the edge was linked (duplicate edges are ignored):
409 Conflict: the edge would close a direct or transitive dependency cycle. A cycle can never resolve, so the plan is unworkable rather than the server being at fault:
500 Internal Server Error:

Example


POST /api/board/:taskId/cancel-dependents

Cancels the still-pending (todo/backlog) transitive dependents of a failed task. A blocked/failed blocker can never become done, so its downstream chain is dead; cancelling it surfaces the stall instead of leaving ghost todo cards that can never become ready. Dependents already in_progress/done/cancelled are left untouched. Each cancelled task emits a status_changed event with reason: 'blocker_failed'. The returned list lets the caller report the stalled plan chain to the leader.
  • Path params: taskId (the failed blocker).
  • Request body: none.

Responses

404 Not Found: the task does not exist:
200 OK: the cancelled dependents (id + title only; may be an empty array):
500 Internal Server Error:

Example


POST /api/board/:taskId/workspace

Provisions a git worktree + branch + system-of-record (SoR) scaffold for a file-mutating task, and records the worktree/branch refs on the task. The worktree lives outside the user’s repo (under the clawboo state dir, namespaced by a hash of the repo path) so it never pollutes the repo’s own git status. Isolation is decided by task kind: code (and unknown kinds) → worktree; research / reviewnone (refused). A repeated provision for a task that already has a live, registered checkout reuses it rather than inserting a duplicate workspace row.
A read-only / research task is refused with 422; it has no file mutations to isolate, so it should not pay the worktree cost.
  • Path params: taskId.
  • Request body (validated by provisionWorkspaceBody):

Responses

400 Bad Request: body failed validation:
404 Not Found: the task does not exist (reason: 'not_found'):
422 Unprocessable Entity: the task kind resolves to no worktree isolation (research/review):
200 OK: the worktree was provisioned (or an existing one reused):
500 Internal Server Error:

Example


GET /api/board/:taskId/workspace

The cold-resume read: the task’s workspace row plus the resume state reconstructed purely from the worktree’s system-of-record (AGENT_HANDOFF.json, falling back to task-progress.md + init.sh), no chat history, no board UI. This is what lets a fresh runtime (or a human) pick up a task from the worktree alone.
  • Path params: taskId.
  • Request body: none.

Responses

404 Not Found: no workspace row for the task (or it has no recorded worktree path):
200 OK: the workspace, the reconstructed resume state, and the parsed handoff (each may be null when the worktree is paused-away or no handoff was written):

Example


PATCH /api/board/:taskId/workspace

Pauses or completes a task’s worktree.
  • pause: commit any uncommitted work, drop the worktree, keep the branch (the workspace stays active and resumable).
  • complete: an empty diff cleans up the worktree + branch and drives the task to done (an empty diff has no deliverable, so the verification gate is intentionally bypassed). A non-empty diff lands the task in in_review, runs the verification gate, then gates →done: only a pass promotes to done; a fail with attempts left reverts to in_progress; a fail at the attempt budget, and completed_with_debt, both route to blocked with a system comment and an inbox notice to the delegator. A repeat complete on an already-blocked task cannot re-enter in_review, so it returns the task’s current status with verified: 'fail' and does not re-run the gate. SoR bookkeeping files are excluded from the diff (a session that only wrote its own progress/handoff is still “empty”).
  • Path params: taskId.
  • Request body (validated by workspaceActionBody):

Responses

400 Bad Request: body failed validation:
404 Not Found: no workspace row (or it has no worktree path / branch):
200 OK (pause): committed-or-not + the resulting HEAD sha:
200 OK (complete): the diff outcome, the resulting task status, and (when the gate ran on a dirty diff) the verification verdict:
500 Internal Server Error:

Example


POST /api/board/:taskId/workspace/handoff

Writes the clock-out AGENT_HANDOFF.json into the task’s worktree, structured data, not prose, so a different runtime (or a human) can pick up the task cleanly. timestamp is defaulted server-side to the current ISO-8601 time when omitted before validation.
  • Path params: taskId.
  • Request body (validated by agentHandoffSchema; timestamp defaulted server-side):

Responses

400 Bad Request: the handoff failed validation:
404 Not Found: no workspace row (or it has no worktree path):
200 OK: the handoff was written:
500 Internal Server Error:

Example


GET /api/board/:taskId/workspace/detail

The task-detail drawer’s Workspace tab: the SoR file contents (TASK.md, task-progress.md, DECISIONS.json, init.sh, VERIFICATION.md, AGENT_HANDOFF.json, only those present) plus the unified diff and diff-stat against the branch-point baseline, with the SoR bookkeeping files excluded.
  • Path params: taskId.
  • Request body: none.

Responses

404 Not Found: no workspace row (or it has no worktree path):
200 OK: the SoR contents + diff (diffStat / diff may be empty when the worktree is paused-away or git is unavailable):
500 Internal Server Error:

Example


Error envelope

Every error response on these routes is the standard envelope { error: string }, except:
  • Body-validation 400s, which add details (the zod flatten() output): { error: 'invalid body', details: {...} } (or { error: 'invalid handoff', details: {...} } on the handoff route).
  • The claim route’s failure paths and the PATCH /:taskId status-failure paths, which return { ok: false, error: <reason> } (reasonnot_found | conflict | illegal_transition | verification_required).
  • The provision route’s failure paths, which return { ok: false, error: 'task not found' } (404) or { ok: false, error: 'no_isolation', isolation } (422).
  • The workspace action / handoff 404s, which return { ok: false, error: 'workspace not found' } (the cold-resume GET and the detail GET use the bare { error: 'workspace not found' }).

See also

  • The board, the state machine, atomic claim, dep chains, and orphan reconciliation
  • Worktrees and handoff, the per-task system-of-record + cross-runtime AGENT_HANDOFF.json
  • Verification, builder≠judge, the deterministic gate + critic, completed_with_debt
  • Runtimes API, POST /api/runtimes/:id/run claims and drives one of these tasks end to end
  • Database schema, the tasks, task_deps, task_comments, workspaces, execution_processes tables
  • REST API overview
Last modified on August 21, 2026