v2.0.0next
HTTP API

Gates, run control, ending and retry

The board's routes for deciding a gate, pausing, resuming, skipping, escalating, ending, archiving and retrying a work item, and the routes a worker calls.

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.

RouteWhat it does
POST /api/work-items/{id}/gates/{gate}/approveApproves the pending gate and starts the chain again.
POST /api/work-items/{id}/gates/{gate}/rejectRejects the pending gate and sends the chain back to work.
POST /api/work-items/{id}/pauseStops a running item and ends its sessions.
POST /api/work-items/{id}/steerStores a note for the next agent launch of a paused item.
POST /api/work-items/{id}/resumeStarts a paused item, with a note if you give one.
POST /api/work-items/{id}/skipAdvances past the current node or gate without running it.
POST /api/work-items/{id}/escalateSends a message to an agent that helps with a stopped item.
POST /api/work-items/{id}/escalate/stopEnds the running escalation turn.
GET /api/work-items/{id}/cancel-previewShows what a cancel would do, without doing it.
POST /api/work-items/{id}/cancelEnds an item and keeps its worktree and branch.
POST /api/work-items/{id}/completeEnds an item as completed, by hand.
POST /api/work-items/{id}/archiveArchives a completed or cancelled item and reclaims its worktree.
POST /api/work-items/{id}/restorePuts an archived item back under Done.
POST /api/work-items/{id}/abandonEnds an item and deletes its worktree and branch.
POST /api/work-items/{id}/retryReruns the node an item stopped on.
POST /api/work-items/{id}/reopen-mrReopens a merge request closed on the forge, then retries.
POST /api/work-items/{id}/progressRecords that a worker started a plan task.
POST /api/work-items/{id}/mr-labelsLabels 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.

FieldTypeDefaultMeaning
digeststringnullThe 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.

StatusWhen
403The 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.
404There is no such item, or {gate} is no gate of its chain.
409The item has ended.
409{gate} is not the gate the item is waiting at.
409The item has review threads labeled must-fix that are not resolved. The answer lists their ids.
409A chain revision gate was approved without the revision's digest, or the revision changed since you read it. Read it again.
409A walk is already running for the item, as when two approvals arrive together. One wins.
422The 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.

FieldTypeDefaultMeaning
notestringrequiredWhy. It is recorded with the rejection and carried into the rerun.
nodestringnullThe 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.

StatusWhen
400node is not a node of the chain before the gate. Nothing changes.
403The 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.
404There is no such item, or {gate} is no gate of its chain.
409The item has ended, or {gate} is not the gate the item is waiting at, or a walk is already running for it.
422The body has no note.
503policy.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. 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.

StatusWhen
403A worker session pauses its own item. This check does not refuse an escalation turn.
404There is no such item.
409The 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.

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.

FieldTypeMeaning
textstringThe 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).

StatusWhen
400text is blank.
404There is no such item.
409The 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.
409The 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.

FieldTypeDefaultMeaning
steerstringnullA note that reaches every paused agent task.
steersobject{}{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"]}
FieldMeaning
node_idThe item's current node, even when a pending rewind starts the walk elsewhere, or null for an item that never started.
steerThe note the relaunch carries, or null. A pending rewind's note goes before yours.
steeredThe canonical paths of the paused agent tasks that were given a note.
StatusWhen
403A worker session resumes its own item. This check does not refuse an escalation turn.
404There is no such item.
409The item has ended, or is not paused (or needs_human on a worker's question). The answer names the way on.
409An escalation turn is running on it, even when the turn is the caller, or a walk is already running.
409Every slot (max_concurrent) is busy.
409A note was sent and no agent task is paused to read it, or none lies ahead.
422steers 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.

FieldTypeDefaultMeaning
notestringnullWhy. It is recorded on the node_skipped event.
pathstringnullOne 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.

StatusWhen
403A worker session skips its own item.
404There is no such item.
409The item is in any other status, or has not started and has no current node.
409An escalation turn is running on it, even when the turn is the caller.
409A walk is already running, as when two skips arrive together. One wins.
409The item's status changed while the skip ran. Try again.
409The node, step, task or gate sets skippable: false, or path is not the node the item stands on or inside it.
422path 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.

FieldTypeDefaultMeaning
messagestringrequiredWhat to tell the agent.
new_threadbooleanfalseStart a fresh agent session instead of continuing the latest thread.

The answer is {"id": "...", "status": "escalating"}.

StatusWhen
400message is blank.
403A worker session escalates its own item.
404There is no such item.
409The item has ended, is neither needs_human nor paused, or has not started.
409A spend cap stopped it, the daily cap included: the agent would hit the same cap. The answer says how to raise it.
409An 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.

StatusWhen
404There is no such item.
409No 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"]
}
FieldMeaning
runningThe item's running session, if any, and when it started.
keptWhat cancel leaves behind. Cancel never removes the worktree or the branch. Archive later removes the worktree but keeps a branch with commits nowhere else.
mrThe item's merge request and its live state, or null before one was ever opened. state is null when the forge cannot answer.
spendThe item's spend and its own cap.
beadsEvery 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.

StatusWhen
404There is no such item.
409The 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.

FieldTypeDefaultMeaning
reasonstringrequiredWhy it ended.
close_mrbooleanfalseClose 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.

StatusWhen
403A worker session of this item. An escalation turn is not refused.
404There is no such item.
409The item has ended.
422reason 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.

FieldTypeDefaultMeaning
reasonstringrequiredWhy it ended.
close_beadsbooleanfalseAlso close the item's issues in beads, as GET /api/work-items/{id}/cancel-preview lists them.

The answer is the stored row.

StatusWhen
403A worker session of this item. An escalation turn is not refused.
404There is no such item.
409The item has ended.
422reason 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.

KeyWhen
kept_branch, unpushed_commitsThe 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_commitsThe worktree's HEAD was detached on commits no branch has. Kraft saved them as kraft/rescued/<id>.
worktree_keptThat branch could not be made, as rescue failed: <reason>. The item is still archived but its worktree stays.
repo_missingThe 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.
StatusWhen
404There is no such item.
409The 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.

StatusWhen
404There is no such item.
409The 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.

StatusWhen
403A worker session abandons its own item. An escalation turn is not refused.
404There is no such item.
409The 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.

FieldTypeDefaultMeaning
steerstringnullText carried into the rerun.
pathstringthe stopped nodeWhat to rerun, by canonical path: node, node.step or node.step.task. A path after the node the item stands on answers 409.
restartbooleanfalseRerun the whole chain from its first node. It cannot be combined with path, task_config or policy.
task_configobjectnullTask fields to change for the retried task, validated against the chain and its policy bounds.
policyobjectnullThe 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}
FieldMeaning
idThe item's id.
node_idThe node the rerun starts at: the one path names, the stopped node, or the first node for a restart.
pathThe canonical path it reruns, or null for a restart.
loopThe key of that node's fix-loop counter, or null for a gate or a node with no fix loop.
steerThe 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.
attemptThe 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.

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.

StatusWhen
403A worker session retries its own item.
404There is no such item.
409The 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.
409The item has ended or has no current node.
409Every slot is busy, or a walk is already running.
409path is after the node the item stands on, or steer is text no agent task downstream of the node would read.
422path, 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, every slot is busy, 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.

StatusWhen
403A worker session reopens its own item's merge request.
404There is no such item.
409The item has ended, is not stopped with stop.kind: "mr_closed", or has no merge request to reopen.
409The reopen worked and the retry was refused, as above.
502The 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.

FieldTypeMeaning
taskintegerThe 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.

StatusWhen
400The item's plan has no ## Task N headings, or task is outside 1 to the number of tasks.
404There is no such item.
409The chain has no implementing node (a node whose own steps hold an agent task with no skill:), or the item is not running it.
409The 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.

FieldTypeMeaning
labelslist of stringsThe labels to set. Each is trimmed, and a blank one is dropped.

The answer is {"work_item_id": "...", "labels": [...]}, the labels as set.

StatusWhen
404There is no such item, or its worktree is not on disk: see The worktree_exists field.
422No label is left once blanks are dropped.
502The forge call failed, with the forge's own message.
Copyright © 2026