Events
Kraft records what happens to a work item as a list of events, its timeline. This page lists every event type, grouped as the contents list shows, and the notification webhook that sends them.
Every event has these fields:
| Field | Meaning |
|---|---|
seq | The event's number on the timeline. |
work_item_id | The item it belongs to. |
type | One of the types below. |
payload | A JSON object. Each type's key fields are in its row. |
node_id | The node the event is about, or null for an item-level event (filing, a pause, a bulk action). It comes from the payload's node_id or node key. A gate's own events carry it under gate. |
created_at | When it happened. |
Before 2.0, an event carried no node_id of its own. The upgrade filled it only from a payload's node_id or node key, so a gate's older events in an upgraded database keep null: they named their node under gate.
Read the events with kraft view events:
| Command | What it shows |
|---|---|
kraft view events ID | The whole timeline. |
kraft view events ID --type gate_requested | Only the events of that type. |
kraft view events ID --after 120 -f | Only the events after seq 120, then follows the timeline. |
kraft view events ID --json | The raw events. |
The tables list each type and the payload fields you are most likely to use. A payload can carry more fields than these. Event types and payloads are not a stable interface: a release can add, rename or reshape them.
Work item
| Type | When | Key fields |
|---|---|---|
work_item_created | The item is filed. | title, repo, chain_template (the chain the filing named, else the repo's default_chain; null when neither names a chain other than default); source, bead_id, priority when auto-intake filed it |
work_item_attachments | The item is filed with a spec or plan. | attachments |
work_item_resumed | A paused item starts or restarts. | steer |
pause_requested | Someone pauses the item. | sessions |
work_item_blocked_by_dependency | An open bead dependency paused the item before its next node. | node_id, blocked_by |
paused_by_broken_base | Another item broke the base branch, so this one paused. | broken_by, follow_up_bead |
work_item_needs_human | The item stopped for a person. See below. | node_id, reason, kind |
work_item_waiting | The item is parked on an external wait. | node_id, retry_at |
work_item_rate_limited | The agent hit a rate limit; the item retries on its own. | node_id, retry_at |
work_item_retried | Someone retried the item. | node_id, steer, escalated (an escalation turn retried it) |
work_item_completed | The item finished. Alone when its chain ended; after work_item_manually_completed when someone completed it by hand. | none |
work_item_manually_completed | Someone completed the item by hand. work_item_completed follows. | reason, node_id |
work_item_cancelled | Someone cancelled the item. work_item_abandoned follows. | reason, node_id |
work_item_abandoned | The item ended as abandoned. Alone for kraft item abandon; after work_item_cancelled for a cancel. | none |
work_item_archived | The item left the Done column. | by, kept_branch, unpushed_commits, rescued_branch, rescued_commits, worktree_kept. See below. |
work_item_restored | An archived item came back. | none |
work_item_title_edited | The title changed. | title |
work_item_description_edited | The description changed. | description |
plan_progress | A worker started a plan task. | node_id, task, total, title |
steer_context_set | A steer was stored for the next agent. | steer |
steer_undelivered | A steer was stored but no agent received it. | steer |
work_item_archived
| Field | Meaning |
|---|---|
by | you or auto. |
kept_branch, unpushed_commits | Present when the branch stayed because it has commits nowhere else. |
rescued_branch, rescued_commits | Present when commits on a detached HEAD were kept on a new branch. |
worktree_kept | The reason, when that branch could not be made and the worktree stayed. |
work_item_needs_human
This is the event behind stop_reason in kraft view show --json, and behind
"why did it stop" on the board. Its fields:
| Field | Meaning |
|---|---|
node_id | The node the item stopped on. |
reason | What stopped it, in words. A reason that starts needs_context: is a question from the agent. |
kind | Why, as one of nine values. See Stop kinds. |
session_id | The last session of that node, when it failed or asked a question. kraft view logs reads its log. |
capped | {cycles, attempts} when a fix loop spent its attempts. |
budget | {scope, spent_usd, cap_usd} when a spend cap stopped the item. |
stuck | true when the item stopped in a way an escalation turn can answer. |
bundle | What Kraft gathered about a stuck item when it gave up. |
suggested_action | {action, reason}, the next step Kraft suggests. |
facts | Detail that depends on the stop, when there is any. See The facts field. |
Stop kinds
kind is also the value stop.kind takes on the HTTP API. Why did my item stop? gives each stop's fix.
| Kind | Meaning |
|---|---|
question | An agent asked for context it needs (needs_context:). |
cap | A loop or time cap ran out: a fix loop's attempts, a time cap, a gate's rejection cycles, a wait's timeout, or rate-limit retries. |
budget | A spend cap, in dollars or tokens, refused the next launch. |
failed | A task failed, a changed-test scope ran red, or a read-only node changed the worktree. |
conflict | A merge or rebase conflict the conflict handler could not clear. |
mr_closed | Someone closed the item's merge request. |
config | The chain, the repo's configuration or a gate's document cannot run as it stands: a task that could not start, a missing base branch, a chain that changed under the walk. |
infra | Kraft or what it depends on failed: a crash, git, the forge, CI infrastructure. facts.cause says which. |
stuck | A loop stopped making progress: the same findings across cycles, a judge's ruling to stop, or a repair that doubts itself. An escalation turn can answer it. |
stop.kind takes three more values. They come from the item's status, not from this event:
| Kind | Meaning |
|---|---|
gate | The item waits at a pending gate (a gate_requested event). |
wait | The item is parked on an external wait: CI or a review. |
rate_limit | An agent rate limit holds the item, and it retries on its own. |
The facts field
What facts carries depends on the stop:
| Stop | facts carries |
|---|---|
A conflict stop | resolved and unresolved: the files the handler cleared and the ones still conflicted (git diff --diff-filter=U in the worktree). |
| A closed MR | Its ref and url. |
A failed stop on a red changed-test-scope run | The first failed command's command, scope, exit_code and session_id (area, and setup: true for an area's setup), and every failed one under failed_scopes when more than one went red. |
| A stop whose cause Kraft can name | cause: see below. |
On an infra stop, cause is one of:
crash: the executor task crashed.stranded: a claim that no walk took over.ci_infra: CI infrastructure failed and retrying did not recover it.git: the worktree refresh failed.forge_auth: the forge refused the credential.forge_unreachable: the forge could not be reached.
For forge_auth and forge_unreachable, the git or forge error says so. A forge task's own failure carries one of these two the same way, on its failed stop, or on its infra one in a node with a fix loop. Other infra stops name no cause.
Chain and nodes
| Type | When | Key fields |
|---|---|---|
chain_loaded | The chain was built for the item. | none |
worktree_prepared | The worktree was created and set up. | report |
node_started | A node began. | node_id |
node_completed | A node finished. | node_id |
node_skipped | A node or its gate was skipped. | node_id, gate, note |
scope_skipped | A step or task inside a node was skipped. | path, note |
node_recovery_started | Kraft reran a node's failed tasks. | node_id, failed_tasks |
run_forked | A retry started a new run from part of the old one. | path, scope, reopened |
base_change_restart | The base branch moved, so part of the chain reran. | node_id, restart_from, nodes |
worktree_rebase_verified | A rebase reported by a worker matched the worktree. | reported_head, worktree_head |
read_only_violated | A read-only node or step changed the worktree. | node_id, scope, files |
fix_cycle_started | A fix loop started another attempt. | node_id, cycle, failed_tasks |
fix_cycle_finished | A fix attempt's repair ran. committed is whether it moved the branch head, null when git could not say. | node_id, cycle, committed, head_sha |
fix_cycle_refunded | A fix attempt did not count, because the repair never ran. | node_id, cycle, outcome |
findings_measured | A review or check produced findings. | node_id, cycle, findings |
test_scopes_selected | A changed-test-scope task picked its commands for a repository, before it ran any. One per repository and round; a rerun emits another, and the latest counts. | node_id, hook_point, repository, round, commands (each area's setup ahead of its first scope) |
judge_verdict | A judge ruled on findings. | node_id, cycle, verdict, reasoning |
sweep_failed | Kraft could not commit work an agent task left uncommitted. The work stays in the worktree. See below. | node_id, task, error |
sweep_left_out | After an agent task, Kraft committed the work it left, but not some paths. See below. | node_id, task, paths |
bead_not_filed | Intake could not file a bead. | reason |
beads_left_open | The item completed but its issues in beads stayed open. | beads, reason |
sweep_failed
Kraft could not commit the work because:
- the commit failed;
- the worktree was not on the item's branch;
- the worktree held a git operation Kraft did not start; or
- the worktree held a repository Kraft did not create.
sweep_left_out
After an agent task, Kraft commits the work the task left, but not the installs the repo's setup made. The event names the paths it left out. It is written only when that set differs from the item's last sweep_left_out, not after every task.
Kraft leaves out:
- a lockfile the repo's setup command wrote and nobody changed since (
uv syncin a repo that commits nouv.lock); - a virtualenv or
node_modulesthe repo does not ignore. Anode_modulesthe setup command made counts whatever is in it: bun leaves no marker.
Kraft commits, as the agent's work:
- a lockfile the agent writes, or an edit it makes to the setup's;
- a
node_modulesthe setup did not make with no package manager's marker in it (.package-lock.json,.modules.yaml,.pnpm,.yarn-integrity,.yarn-state.yml), even beside apackage.json; - a
pyvenv.cfgwith nopythonbeside it.
Gates and review
| Type | When | Key fields |
|---|---|---|
gate_requested | A gate is waiting for a decision. | gate, node_id |
gate_approved | A gate was approved. | gate, by |
gate_rejected | A gate was rejected. | gate, note, node, by |
gate_reopened | An approved gate is open again. | gate, reason |
gate_auto_review_started | A gate's review agent started. | gate, session_id |
gate_auto_review_skipped | A gate's review agent did not run, or left the decision to a person. | gate, reason; note says why, when the review gave a reason |
gate_auto_review_discarded | A review agent's verdict was not used. | gate, verdict, reason |
chain_revision_shown | A person was shown a chain revision. | gate, digest |
chain_revision_unchanged | A chain revision proposed no change, so its gate passed. | gate, rationale |
chain_revised | An approved chain revision, or an applied item draft, replaced the chain. | gate, changes, diff; source: "draft" and gate: null for an item draft |
artifact_refused | Kraft found a gate's document, or a document a task wrote, but would not serve it; once per refused request. | path, reason |
review_submitted | A review was sent. | review_id, gate, outcome |
thread_updated | A review thread was resolved or reopened. | thread_id, state |
rewind_requested | A review asked for work to be redone. | review_id, target, note |
rewind_cancelled | That request was withdrawn. | review_id |
reply_agent_wrote | An agent answered review threads. | gate, files |
reply_agent_skipped | No agent answered review threads. | gate, reason |
reply_agent_failed | The reply agent failed. | gate, error |
by is human (the board or the kraft command), assistant (an agent
through the MCP server), agent (a Kraft worker, such as a gate's review
agent) or kraft (Kraft itself).
Agent sessions
| Type | When | Key fields |
|---|---|---|
worker_session_created | A session was recorded, before launch. | session_id, node_id, hook_point, attempt |
worker_session_started | Its process started. | session_id, node_id, hook_point, pid |
worker_session_exited | It ended. | session_id, status, wall_ms, model, cost_usd, token counts |
worker_session_paused | It was stopped by a pause, skip or manual end. | session_id |
agent_session_resumed | A session resumed the agent's earlier conversation. | task, session_id |
session_reattached | After a restart, Kraft found a session still running. | session_id, pid |
session_unknown | After a restart, Kraft could not confirm a session. | session_id, reason |
background_jobs_abandoned | A session left background jobs behind. | reason |
permission_decision | The permission gate answered a tool call. | session_id, node_id, tool, decision, reason, grant |
Caps, budgets and limits
| Type | When | Key fields |
|---|---|---|
time_cap_reached | A time cap ran out. | scope, field, minutes, reason, node_id |
scope_budget_reached | A scope's token or dollar cap refused a launch. | the breach fields, task |
budget_changed | The item's spend cap was set. | budget_usd |
budget_raised | The cap was raised to continue a stopped item. | budget_usd; key: "policy.budget_usd" when it was the item-wide budget_usd of the item's policy, not its own cap |
spend_unpriced | The first session on the item whose harness reported no cost and whose model prices.json cannot price: the item and daily dollar caps count it as $0. | session_id, harness, model, message |
cap_counters_reset | Loop counters were reset. | by, counters |
rate_limit_hit | An agent CLI refused a launch for a rate limit. | node_id, rate_limit_type, resets_at_iso |
launch_fallback | A launch moved to the next harness or model. | node_id, task, from, to, reason |
launch_fallback_exhausted | Every fallback was limited. | node_id, task, resets_at_iso |
Escalation
| Type | When | Key fields |
|---|---|---|
escalation_message | A message went into the escalation thread. | session_id, message, auto, thread, turn |
stuck_escalation_started | Kraft started an escalation turn on a stuck item. | node_id, task, reason, attempt |
stuck_escalation_finished | That turn ended. | node_id, task, status |
work_item_auto_escalate_skipped | Kraft did not escalate a stop. | reason |
work_item_auto_escalate_capped | Automatic escalations hit their limit. | cap, count |
work_item_self_retry_requested | An escalation turn asked to retry the item. | node_id, path, restart, steer |
work_item_self_retry_dropped | That retry did not happen. | node_id, status |
Forge, CI and waits
| Type | When | Key fields |
|---|---|---|
mr_opened | The merge request was opened. | number, url |
mr_closed | Kraft closed the item's merge request because POST /api/work-items/{id}/cancel set close_mr: true. One someone else closed is a work_item_needs_human event of kind mr_closed. | ref, url, by (always "cancel") |
mr_reopened | POST /api/work-items/{id}/reopen-mr reopened a closed merge request. | ref, url |
mr_labels_set | A worker labeled the merge request. | labels |
external_wait_started | A wait on CI or a review began. | task, node_id, kind, timeout_s, deadline |
external_wait_observed | Kraft checked the wait. | task, observation, state, result, next_observation_at |
external_wait_rebounded | A policy change moved the wait's limits. | task, timeout_s, deadline |
external_wait_ended | The wait settled, timed out or errored. | task, outcome, result, observations |
ci_infra_exhausted | CI failed for its own reasons, and retries did not help. | node_id, reason |
ci_run_abandoned | A cancelled CI run has no successor. | node_id, reason |
ci_not_configured | The repo sets ci_checks: false, so a CI wait, before or after the merge, passed without checking. | node_id, task, repo |
automated_review_not_configured | The repo names no automated reviewer, so the wait settles without one. | node_id, task, repo |
automated_review_errored | The automated reviewer failed. | node_id, reason |
Sandbox
| Type | When | Key fields |
|---|---|---|
sandbox_egress_refused | The sandbox proxy refused a host. | session_id, host, port, phase, reason |
sandbox_oom_killed | The container was killed under its memory limit. confirmed is false when the runtime never flagged the kill and Kraft counted exit 137 under the limit instead. | session_id, memory, confirmed |
sandbox_branch_not_synced | The sandbox's branch did not reach the worktree. | session_id, branch, reason |
sandbox_kit_resolved | A task was dispatched under a Kit, once per item and Kit. skipped lists the optional capabilities Kraft does not enforce, and ignored those it accepts without applying (agent-sessions@1, a credential's oauth). | kit, manifest, skipped, ignored |
A workspace item records one sandbox_branch_not_synced for each repository whose branch did not move, and its reason names that repository.
Settings changed
| Type | When | Key fields |
|---|---|---|
chain_template_changed | The item moved to another chain before it started. | from, to |
attachments_changed | Its spec or plan changed before it started. | from, to |
agent_overrides_changed | Its model or effort override changed. | overrides |
node_overrides_changed | A node's override changed. | overrides |
policy_override_changed | Its policy override changed. | policy |
notification_failed | The notification webhook failed twice. | event_type, status, host, error |
Before 2.0, a chain was a chain template. chain_template_changed, and the chain_template key of work_item_created, keep that name, and both hold chain ids.
Reading events as they happen
WS /api/ws/events
Streams every work item's events as they are stored. The board and kraft view watch read it. Unlike GET /api/work-items/{id}/events, it is not limited to one item. It first replays the stored events after after_seq, then sends each new event once, with none lost or repeated between the two.
| Query | Type | Default | Meaning |
|---|---|---|---|
after_seq | integer | 0 | Replay every stored event with a greater seq first. 0 replays the whole timeline of every item, so reconnect with the last seq you saw. |
live | boolean | false | 1 or true also sends live messages. |
Each frame is one JSON text message. An event frame has the fields in the table above, {seq, work_item_id, type, payload, node_id, created_at}. A live message has {"frame": "live", "type", "payload", "created_at"} and no seq.
A client authenticates as for the HTTP routes: who may call. A peer on this machine needs nothing while Kraft is bound to loopback. Otherwise send the MCP bearer token as Authorization: Bearer <token>, or the session cookie from the login page. The trigger token is not accepted. The Host and Origin rules of the HTTP routes apply too.
| Refusal or close | When |
|---|---|
403 on the handshake | Kraft refused the connection before accepting it. See below. |
Close code 1011 | The client fell behind: more than 1000 messages were waiting for it. Reconnect with after_seq set to the last seq you read. |
A refused connection closes before it is accepted, so a real client sees the handshake fail with HTTP 403, not a close frame. A test client reports it as close code 1008. The causes are no valid credential, a remote caller while no password is set, a Host that is not allowed, and an Origin from another site.
Live messages
A client of WS /api/ws/events that connects with live=1 also receives messages
that are not events. They are {"frame": "live", "type", "payload", "created_at"},
have no seq, are sent once to whoever is connected and are neither stored nor
replayed after a reconnect. A client that does not ask for them never sees one.
| Type | When | Key fields |
|---|---|---|
intake_checked | An intake poll found a bead ready, started an item or left one alone. A poll that found nothing sends none. | id, at, ready, started, skipped |
apply_changed | What is saved but not applied changed: a draft was published, a reload or an access save ran, or a file was edited on disk (checked every 5 seconds). Not sent when the pending items are the same. | restart, reload (how many of each; see GET /api/apply) |
Notifications
notify.yaml sends an HTTP POST to a webhook when an event you name
happens. Set it on the Settings screen, or edit the file:
enabled: true
url: https://hooks.example.com/abc123 # a secret; Kraft never shows it back
base_url: https://kraft.example.com # where links in the message point
events: [gate_requested, work_item_needs_human]
notify.yaml lists the fields, and what events accepts. Get notified when an item needs you sets it up.
The body is JSON:
{
"work_item_id": "<work-item-id>",
"title": "Fix the flaky login test",
"type": "gate_requested",
"gate": "spec_approval",
"url": "https://kraft.example.com/work-items/<work-item-id>"
}
gate is null for an event that is not about a gate.
- Kraft sends at most one message per work item every 10 seconds. A second matching event in that window sends nothing.
- A send that times out after 5 seconds, errors, or gets a status of 400 or
more is tried once more after 2 seconds. If that fails too, Kraft records a
notification_failedevent on the item. - Events that happen while the server is down send nothing when it comes back.
- The Settings screen's Send a test posts
"type": "test"with anullwork_item_idandgate.
Related
- Why did my item stop?: each stop's reason and its fix.
- Work item statuses: the statuses the events move an item between.
- Budget, events, diff and documents: reading a work item's events over HTTP, paged forward or backward. The WebSocket is
WS /api/ws/eventsabove. - Inbound triggers: the other direction, starting a chain from outside.