Gates, run control, ending and retry
This page lists the board's routes that act on a work item once it is filed: they decide its gates, control its run, end it and retry it. The routes that file, read and change an item are on Filing, reading and changing work items, which also says that every board route can change in any release. A route here that answers with the item itself answers the stored row, never the detail object.
| Route | What it does |
|---|---|
POST /api/work-items/{id}/gates/{gate}/approve | Approves the pending gate and starts the chain again. |
POST /api/work-items/{id}/gates/{gate}/reject | Rejects the pending gate and sends the chain back to work. |
POST /api/work-items/{id}/pause | Stops a running item and ends its sessions. |
POST /api/work-items/{id}/unblock | Drops what a blocked or paused item still comes after. |
POST /api/work-items/{id}/steer | Stores a note for the next agent launch of a paused item. |
POST /api/work-items/{id}/resume | Starts a paused item, with a note if you give one. |
POST /api/work-items/{id}/skip | Advances past the current node or gate without running it. |
POST /api/work-items/{id}/escalate | Sends a message to an agent that helps with a stopped item. |
POST /api/work-items/{id}/escalate/stop | Ends the running escalation turn. |
GET /api/work-items/{id}/cancel-preview | Shows what a cancel would do, without doing it. |
POST /api/work-items/{id}/cancel | Ends an item and keeps its worktree and branch. |
POST /api/work-items/{id}/complete | Ends an item as completed, by hand. |
POST /api/work-items/{id}/archive | Archives a completed or cancelled item and reclaims its worktree. |
POST /api/work-items/{id}/restore | Puts an archived item back under Done. |
POST /api/work-items/{id}/abandon | Ends an item and deletes its worktree and branch. |
POST /api/work-items/{id}/retry | Reruns the node an item stopped on. |
POST /api/work-items/{id}/reopen-mr | Reopens a merge request closed on the forge, then retries. |
POST /api/work-items/{id}/progress | Records that a worker started a plan task. |
POST /api/work-items/{id}/mr-labels | Labels the item's merge request. |
Deciding a gate
POST /api/work-items/{id}/gates/{gate}/approve
Approves the pending gate {gate} and starts the chain again at the node after it. kraft item approve calls it. {gate} is the id of a gate node of the item's chain. A review agent still running on the gate is stopped first: a person's decision outranks it. The decision is recorded with who made it: see Events.
The body is optional.
| Field | Type | Default | Meaning |
|---|---|---|---|
digest | string | null | The digest of the chain revision you read, as kraft view artifact printed it. Only a gate that approves a chain revision takes one, and needs it when the revision changes the chain. |
The answer is the stored row: the item's columns as saved, without display_status, stop or the other fields the detail adds.
| Status | When |
|---|---|
403 | The request carries the X-Kraft-Session-Id of a worker session of this item, or of its escalation turn. A gate is a person's call. |
404 | There is no such item, or {gate} is no gate of its chain. |
409 | The item has ended. |
409 | {gate} is not the gate the item is waiting at. |
409 | The item has review threads labeled must-fix that are not resolved. The answer lists their ids. |
409 | A chain revision gate was approved without the revision's digest, or the revision changed since you read it. Read it again. |
409 | A walk is already running for the item, as when two approvals arrive together. One wins. |
422 | The gate's required document is missing, or a chain revision cannot be applied. The detail says why, and the item stops as config: retry the node that owes the document. |
The final review gate always needs its document, and any other gate does when it sets artifact_required: true. The 403 check reads the header, so a caller that leaves it off passes: see Agents and gates.
POST /api/work-items/{id}/gates/{gate}/reject
Rejects the pending gate {gate} and sends the chain back to work, with your note as the steer. kraft item reject calls it. A review agent still running on the gate is stopped first. Each rejection counts against the gate's reject loop. Once that cap is spent, the item stays at the gate, stopped as cap, and nothing reruns: a person decides.
| Field | Type | Default | Meaning |
|---|---|---|---|
note | string | required | Why. It is recorded with the rejection and carried into the rerun. |
node | string | null | The node the chain re-enters at. Left out: the gate's reject_to, else the execution node before the gate, else the gate itself. It must be a node of the chain before the gate. |
The answer is the stored row.
| Status | When |
|---|---|
400 | node is not a node of the chain before the gate. Nothing changes. |
403 | The request carries the X-Kraft-Session-Id of a worker session of this item, or of its escalation turn. A gate is a person's call. |
404 | There is no such item, or {gate} is no gate of its chain. |
409 | The item has ended, or {gate} is not the gate the item is waiting at, or a walk is already running for it. |
422 | The body has no note. |
503 | policy.yaml does not load. Nothing changes: a rerun Kraft cannot bound is not started. |
Pausing and resuming
POST /api/work-items/{id}/pause
Stops a running item: its running sessions are ended and the item becomes paused. kraft item pause calls it. It takes an item that is active, waiting on CI or a review, or rate_limited, and clears the timer that would wake a waiting or rate-limited item. It also takes a queued or blocked item out of the hold, back to the status it came from, with paused_sessions empty. A blocked item keeps its dependencies. The body is empty.
{"id": "...", "paused_sessions": ["..."]}
paused_sessions holds the ids of the sessions it ended, and is empty for an item with none running.
| Status | When |
|---|---|
403 | A worker session pauses its own item. This check does not refuse an escalation turn. |
404 | There is no such item. |
409 | The item is in any other status, an ended one included. An escalation turn gets this too: it runs only on an item that is needs_human or paused, which pause does not take. |
An item the queue is starting answers 409, with work item is being started from the queue: pause it again in a moment. A second pause stops it once it runs. A held item that something else took out in that instant answers 409 with work item status changed; try again.
POST /api/work-items/{id}/unblock
Stops an item coming after what it still waits on. kraft item unblock calls it. It takes a blocked item and a paused one, so an item can be freed before it is started. It starts nothing: a blocked item with nothing left to wait for is released to the queue by the scheduler's next pass.
| Field | Type | Default | Meaning |
|---|---|---|---|
dependency | string | null | Drop only this dependency. Absent, every one the item still waits on; a met one stays on the record. |
{"id": "...", "dropped": ["..."], "waiting_on": [{"id": "...", "title": "...", "status": "active"}]}
The event is work_item_dependencies_dropped.
| Status | When |
|---|---|
403 | A worker session unblocks its own item. |
404 | There is no such item. |
409 | The item is in any status but blocked or paused, or dependency is not one of its dependencies. Nothing is dropped. |
POST /api/work-items/{id}/steer
Stores a note for the next agent launch of a paused item. A POST /api/work-items/{id}/resume that carries steer stores it and resumes in one call. The note is stored once and leads the prompt of the launch that takes it.
| Field | Type | Meaning |
|---|---|---|
text | string | The note. Surrounding whitespace is dropped. |
The answer is {"id": "...", "steer": "..."}. It takes a paused item, and a needs_human item stopped on a worker's question (stop.kind question).
| Status | When |
|---|---|
400 | text is blank. |
404 | There is no such item. |
409 | The item has ended, or is in any other status. The answer names the way on: pause a running item first, approve or reject at a gate, retry an item that stopped. |
409 | The pause stopped no agent task, such as a test run or a CI wait, so no agent would read the note. |
POST /api/work-items/{id}/resume
Starts a paused item, including one just filed, at the node it stood on. kraft item resume calls it. The worktree is first brought up to date with its base branch. A conflict goes to the node's on_conflict handler or stops the item as conflict. Another git failure stops it as infra, and the answer is then the stored row.
With every slot (max_concurrent) busy it answers 200 with {"id": "...", "status": "queued", "slots": {"busy": 3, "limit": 3}}. The item waits as queued, and Kraft makes the same request when a slot frees.
An item that comes after an unfinished item answers 200 with {"id": "...", "status": "blocked", "waiting_on": [{"id", "title", "status"}]} instead, ahead of the slot check. One that comes after an abandoned item is refused with 409, naming it and the two ways on: unblock the item or abandon it.
| Field | Type | Default | Meaning |
|---|---|---|---|
steer | string | null | A note that reaches every paused agent task. |
steers | object | {} | {task path: note}: a note for one paused agent task, which wins over steer for that task. |
{"id": "...", "node_id": "implementation", "steer": "...", "steered": ["implementation.main.implement"]}
| Field | Meaning |
|---|---|
node_id | The item's current node, even when a pending rewind starts the walk elsewhere, or null for an item that never started. |
steer | The note the relaunch carries, or null. A pending rewind's note goes before yours. |
steered | The canonical paths of the paused agent tasks that were given a note. |
| Status | When |
|---|---|
403 | A worker session resumes its own item. This check does not refuse an escalation turn. |
404 | There is no such item. |
409 | The item has ended, or is not paused (or needs_human on a worker's question). The answer names the way on. |
409 | An escalation turn is running on it, even when the turn is the caller, or a walk is already running. |
409 | A note was sent and no agent task is paused to read it, or none lies ahead. |
422 | steers names a path that is not a paused agent task. |
Skipping and escalating
POST /api/work-items/{id}/skip
Advances past the current node, or the gate the item waits at, without running or approving it, and starts the chain at the next node. kraft item skip calls it.
With a path that names a step or a task, only that scope is skipped: an active item keeps its walk, which counts the scope as done, and any other item resumes from where it stood. Skipping a gate does not approve it: the event is node_skipped, with the gate's id, and no gate_approved.
A running node's sessions are ended first. It takes an item that is active, waiting, paused or needs_human.
| Field | Type | Default | Meaning |
|---|---|---|---|
note | string | null | Why. It is recorded on the node_skipped event. |
path | string | null | One node, step or task of the node the item stands on, by canonical path. Only the sessions inside it stop. Left out: the current node or pending gate. |
The answer is the stored row.
| Status | When |
|---|---|
403 | A worker session skips its own item. |
404 | There is no such item. |
409 | The item is in any other status, or has not started and has no current node. |
409 | An escalation turn is running on it, even when the turn is the caller. |
409 | A walk is already running, as when two skips arrive together. One wins. |
409 | The item's status changed while the skip ran. Try again. |
409 | The node, step, task or gate sets skippable: false, or path is not the node the item stands on or inside it. |
422 | path names nothing in the item's chain. |
POST /api/work-items/{id}/escalate
Sends a message to an agent that helps with a stopped item: it starts an escalation turn, or continues the item's thread. kraft item escalate calls it. The turn runs after the answer. When the turn asks to retry its own item, Kraft performs that once the turn ends.
| Field | Type | Default | Meaning |
|---|---|---|---|
message | string | required | What to tell the agent. |
new_thread | boolean | false | Start a fresh agent session instead of continuing the latest thread. |
The answer is {"id": "...", "status": "escalating"}.
| Status | When |
|---|---|
400 | message is blank. |
403 | A worker session escalates its own item. |
404 | There is no such item. |
409 | The item has ended, is neither needs_human nor paused, or has not started. |
409 | A spend cap stopped it, the daily cap included: the agent would hit the same cap. The answer says how to raise it. |
409 | An escalation turn is already running, even when the turn is the caller, or a walk is, such as a gate's review agent. |
POST /api/work-items/{id}/escalate/stop
Ends the running escalation turn. The item keeps the status and stop it had: only the turn ends. The body is empty.
{"id": "...", "session_id": "...", "status": "paused"}
session_id is the turn's session, and status is always paused.
| Status | When |
|---|---|
404 | There is no such item. |
409 | No escalation turn is running. |
Ending a work item
GET /api/work-items/{id}/cancel-preview
Shows what POST /api/work-items/{id}/cancel would do, without doing it.
{
"running": {"node": "mr_checks", "task": "mr_checks", "attempt": 1, "started_at": "2026-10-03T10:00:00+00:00"} | null,
"kept": {"branch": "kraft/...", "worktree": "/path", "findings": 0, "threads": 1},
"mr": {"ref": 54, "url": "https://...", "state": "open"} | null,
"spend": {"spent_usd": 1.2, "cap_usd": 5.0},
"beads": ["kraft-ab1"]
}
| Field | Meaning |
|---|---|
running | The item's running session, if any, and when it started. |
kept | What cancel leaves behind. Cancel never removes the worktree or the branch. Archive later removes the worktree but keeps a branch with commits nowhere else. |
mr | The item's merge request and its live state, or null before one was ever opened. state is null when the forge cannot answer. |
spend | The item's spend and its own cap. |
beads | Every bead POST /api/work-items/{id}/complete closes when asked with close_beads. See below. |
beads holds the item's own bead, the ones it implements, and those a Fixes or Closes trailer on its commits names (an id starting Kraft-, any case). An item filed while bd was down has none yet, and close_beads files one for it and closes that.
| Status | When |
|---|---|
404 | There is no such item. |
409 | The item has ended, as for /cancel. |
POST /api/work-items/{id}/cancel
Ends the item, with a reason, and stops whatever is running. The worktree and the branch stay, and so does the merge request unless close_mr is set. The item's stored status becomes abandoned, which the board shows as cancelled: see Displayed status.
| Field | Type | Default | Meaning |
|---|---|---|---|
reason | string | required | Why it ended. |
close_mr | boolean | false | Close the item's merge request on the forge once the cancel has landed, if it is open. |
The answer is the stored row. With close_mr: true it also carries close_mr: {"ok": true}, or {"ok": false, "error": "..."} when there was no open merge request or the forge call failed. A failed close never undoes the cancel. The key is absent when close_mr was not asked for.
| Status | When |
|---|---|
403 | A worker session of this item. An escalation turn is not refused. |
404 | There is no such item. |
409 | The item has ended. |
422 | reason is blank. |
POST /api/work-items/{id}/complete
Ends the item as completed, by hand, with a reason, and stops whatever is running. kraft item complete calls it. Nothing runs and no gate is approved: the worktree, the branch and the merge request stay as they are. The reason goes on the timeline, as work_item_manually_completed and then work_item_completed. The item's beads stay open unless close_beads is set.
| Field | Type | Default | Meaning |
|---|---|---|---|
reason | string | required | Why it ended. |
close_beads | boolean | false | Also close the item's issues in beads, as GET /api/work-items/{id}/cancel-preview lists them. |
The answer is the stored row.
| Status | When |
|---|---|
403 | A worker session of this item. An escalation turn is not refused. |
404 | There is no such item. |
409 | The item has ended. |
422 | reason is blank. |
POST /api/work-items/{id}/archive
Archives a completed or cancelled item and reclaims its worktree. The item still reads as completed or cancelled. It answers {"id", "archived_by", "worktree_removed"}. An item already archived answers the same shape with worktree_removed: false.
Archive never deletes a commit, and keeps the attachment copies. It does discard uncommitted changes in the worktree, which it removes with git worktree remove --force. These keys appear only in the cases below.
| Key | When |
|---|---|
kept_branch, unpushed_commits | The branch was kept because some of its commits are on no other branch, remote-tracking branch or tag, and not in what Kraft pushed. |
rescued_branch, rescued_commits | The worktree's HEAD was detached on commits no branch has. Kraft saved them as kraft/rescued/<id>. |
worktree_kept | That branch could not be made, as rescue failed: <reason>. The item is still archived but its worktree stays. |
repo_missing | The item's repository was moved or deleted. Archive removed the worktree directory directly and did nothing to the branch: if you only moved the repository, it is still there. |
| Status | When |
|---|---|
404 | There is no such item. |
409 | The item is in any status but completed or abandoned (shown as cancelled). |
POST /api/work-items/{id}/restore
Puts an archived item back under Done. The item's status never moved when it was archived, so it reads as completed or cancelled again. Restore clears archived_at and archived_by and records work_item_restored. It does not bring back the worktree that archive removed. The body is empty.
{"id": "...", "status": "completed"}
status is the item's stored status, completed or abandoned.
| Status | When |
|---|---|
404 | There is no such item. |
409 | The item is not archived. |
POST /api/work-items/{id}/abandon
Ends the item and deletes its worktree, branch and attachment copies. Uncommitted work and unpushed commits are lost. It answers {"id", "status": "abandoned", "worktree_removed"}.
When the item's repository has been moved or deleted, abandon removes the worktree directory directly and adds repo_missing, the repository path it could not find. It does nothing to the branch.
| Status | When |
|---|---|
403 | A worker session abandons its own item. An escalation turn is not refused. |
404 | There is no such item. |
409 | The item is active. Pause it first. |
An item that is already abandoned, a cancelled one included, is reclaimed again without a second event. A second call finds nothing left and answers worktree_removed: false.
Retrying a work item
POST /api/work-items/{id}/retry
Reruns the node an item stopped on, with steer text if you give it. It is the only way back onto an item stopped by a task failure, and kraft item retry calls it.
Only a retry from a person resets the cap counters of what it reruns. A retry that carries X-Kraft-Session-Id or X-Kraft-Client: mcp leaves them where they stand, so an item stopped on a cap stops again.
| Field | Type | Default | Meaning |
|---|---|---|---|
steer | string | null | Text carried into the rerun. |
path | string | the stopped node | What to rerun, by canonical path: node, node.step or node.step.task. A path after the node the item stands on answers 409. |
restart | boolean | false | Rerun the whole chain from its first node. It cannot be combined with path, task_config or policy. |
task_config | object | null | Task fields to change for the retried task, validated against the chain and its policy bounds. |
policy | object | null | The retried scope's policy, validated the same way. |
With no path and no restart, a pending rewind replaces path, and its note goes before your steer. A rewind is a request-changes review submitted while no gate was pending.
{"id": "...", "node_id": "implement", "path": "implement", "loop": null, "steer": null, "attempt": 2}
| Field | Meaning |
|---|---|
id | The item's id. |
node_id | The node the rerun starts at: the one path names, the stopped node, or the first node for a restart. |
path | The canonical path it reruns, or null for a restart. |
loop | The key of that node's fix-loop counter, or null for a gate or a node with no fix loop. |
steer | The text the rerun carries: your steer (with a pending rewind's note before it), else the unresolved findings of the last review, else the last rejection's note, else null. |
attempt | The attempt number the task it relaunches will run as: the count of that task's own sessions so far, plus one. |
A path naming a task counts only that task's sessions, and one naming a step takes the highest next attempt over the step's tasks. A path naming a node, which is the default, takes the highest over the node's own tasks: all of its tasks except its escalation task, which is never counted. A restart does the same for the first node. A walk may enter at any one of those tasks.
A retry that stops at once answers the stored row.
With every slot busy, a retry answers 200 with {"id": "...", "status": "queued", "slots": {"busy": 3, "limit": 3}}, and the item waits as queued. Kraft makes the same request when a slot frees. An item that comes after an unfinished item answers 200 with status: "blocked" and waiting_on first, and 409 when what it comes after was abandoned. /reopen-mr queues the same way, after it reopens the merge request.
An escalation turn that retries its own item is not refused. Kraft records the request and performs it once the turn ends, and the answer is {id, node_id, loop, steer}, with no path or attempt.
| Status | When |
|---|---|
403 | A worker session retries its own item. |
404 | There is no such item. |
409 | The item's status is not needs_human, so it is not stopped. A waiting or rate_limited item has a stop and still answers 409, and a paused one is told to resume. |
409 | The item has ended or has no current node. |
409 | A walk is already running. |
409 | path is after the node the item stands on, or steer is text no agent task downstream of the node would read. |
422 | path, task_config or policy is refused. |
POST /api/work-items/{id}/reopen-mr
Reopens the item's merge request on the forge and retries the stopped node, the same as POST /api/work-items/{id}/retry with no path. Use it on an item that stopped because its merge request was closed there without merging.
A poller checks every waiting or needs_human item parked at a merge-request node against the forge every policy.forge_poll_s seconds (default 300). When the merge request was closed without merging, it stops the item with stop.kind: "mr_closed" and facts: {ref, url}. See Events: work_item_needs_human.
There are three ways out: this route, POST /api/work-items/{id}/retry with the merge request node's path to open a new merge request, and POST /api/work-items/{id}/cancel. Cancelling leaves the closed merge request and the worktree as they are. MR closed externally lists the board's button for each.
The answer is that of /retry with no path, and so are its 409s. The ones that matter here: the item has no current node, or a walk is already running.
The route reopens the merge request on the forge and records mr_reopened before it retries. If the retry then answers 409, the merge request stays reopened and the item stays stopped. Repeating this call asks the forge to reopen it again, which may fail: retry the item with POST /api/work-items/{id}/retry instead.
| Status | When |
|---|---|
403 | A worker session reopens its own item's merge request. |
404 | There is no such item. |
409 | The item has ended, is not stopped with stop.kind: "mr_closed", or has no merge request to reopen. |
409 | The reopen worked and the retry was refused, as above. |
502 | The forge call failed, with the forge's own message. The item stays stopped. |
Routes a worker calls
A worker session calls these on its own item. Neither route refuses a worker session. A sandboxed worker under a network: policy reaches progress through its channel but not mr-labels: the channel allows only a short list of routes (see Agents and gates).
POST /api/work-items/{id}/progress
Records that the implementing worker started plan task task, as a plan_progress event. kraft item progress calls it.
| Field | Type | Meaning |
|---|---|---|
task | integer | The N of the plan's ## Task N heading the worker is starting. |
The answer is {"id": "...", "progress": {...}}, where progress is the progress field with its tasks, or null when the item has no plan to follow.
| Status | When |
|---|---|
400 | The item's plan has no ## Task N headings, or task is outside 1 to the number of tasks. |
404 | There is no such item. |
409 | The chain has no implementing node (a node whose own steps hold an agent task with no skill:), or the item is not running it. |
409 | The run is rework after a gate rejection: it follows the rejection's note, not the plan. |
POST /api/work-items/{id}/mr-labels
Labels the item's merge request on the forge and re-creates its pipeline, then records mr_labels_set. kraft item mr-label calls it. A repair agent calls it once it has read a failed CI trace and decided which labels the trace asks for.
| Field | Type | Meaning |
|---|---|---|
labels | list of strings | The labels to set. Each is trimmed, and a blank one is dropped. |
The answer is {"work_item_id": "...", "labels": [...]}, the labels as set.
| Status | When |
|---|---|
404 | There is no such item, or its worktree is not on disk: see The worktree_exists field. |
422 | No label is left once blanks are dropped. |
502 | The forge call failed, with the forge's own message. |
Filing, reading and changing work items
The board's routes for filing a work item, duplicating it, reading the board and one item, changing an item, and acting on several at once.
The work item object
The three shapes a work item answers in, its stored columns, and the fields that need an explanation, from its status and stop to its budget cap and plan progress.