@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.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.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):
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 / review → none (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 staysactiveand resumable). -
complete: an empty diff cleans up the worktree + branch and drives the task todone(an empty diff has no deliverable, so the verification gate is intentionally bypassed). A non-empty diff lands the task inin_review, runs the verification gate, then gates→done: only apasspromotes todone; afailwith attempts left reverts toin_progress; afailat the attempt budget, andcompleted_with_debt, both route toblockedwith a system comment and an inbox notice to the delegator. A repeatcompleteon an already-blockedtask cannot re-enterin_review, so it returns the task’s current status withverified: '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;timestampdefaulted 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 zodflatten()output):{ error: 'invalid body', details: {...} }(or{ error: 'invalid handoff', details: {...} }on the handoff route). - The
claimroute’s failure paths and thePATCH /:taskIdstatus-failure paths, which return{ ok: false, error: <reason> }(reason∈not_found|conflict|illegal_transition|verification_required). - The
provisionroute’s failure paths, which return{ ok: false, error: 'task not found' }(404) or{ ok: false, error: 'no_isolation', isolation }(422). - The
workspaceaction / 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/runclaims and drives one of these tasks end to end - Database schema, the
tasks,task_deps,task_comments,workspaces,execution_processestables - REST API overview