Skip to main content
Accepted agent webhook work is recorded as a work item before its runtime starts. Chat, routine webhooks and other producers are not yet covered by this queue. The item is the durable answer to “did you take this, and what happened to it” — it survives a restart, it keeps its identity across every retry, and it carries the authorization it was admitted under. Three identifiers get conflated constantly, so they are worth separating once: A fourth value, generation, is bumped on every claim. It is the fence: a worker whose lease was taken over cannot report a result for the attempt that replaced it.

States

needs_reconciliation is not a failure and not an automatic retry. It means a process may still be alive under a locator nobody has verified, so it keeps holding its capacity until a person or a reconciler resolves it. A timer never clears it. cancelled means a confirmed stop. Work whose process could not be stopped, or whose outcome is unclear, goes to needs_reconciliation instead — never to cancelled.

What these responses never contain

The ledger stores the accepted input (input_json), each transition’s free-form detail, and — for a webhook — the raw payload. None of it is served here. A work item’s input is, for a chat or assignment source, the conversation itself, so the API returns its fingerprint (input_sha256) and the revision it was accepted against instead. No response on this page carries conversation text, credentials or model output. A receipt is an identifier, not a capability. Holding a work_id or a delivery_id grants nothing: every route below requires an authenticated session or CLI token, membership of {workspaceId}, and — for the two POST routes — MANAGER or above. A webhook token is permission to deliver, and never permission to read the work a delivery produced.

Reading work items

The list is keyset paginated: at most 100 rows, with next_cursor set when more remain, passed back as ?after=. Filters are validated against the vocabularies above — ?state=runnign is a 400, not an empty page, because “nothing is running” is the wrong answer to a typo. A work item belonging to another workspace answers 404, not 403: a 403 would confirm the id exists to someone who has no business knowing it. The detail adds two lists. attempts[] carries one entry per attempt — run_id (the same namespace as the journal’s run id), the lease and its heartbeat, the runtime_locator recovery consults before deciding a process is gone, and cost_usd. events[] is the append-only history in sequence order.

Cancelling

Cancel is an idempotent request, not an immediate terminal value. The outcome field says what actually happened, and a client must not treat the three cases as the same 200: state is always the item’s true state after the call. Cancelling does not undo external effects that already happened. Repeating the call is safe. A cancel request against a live attempt is recorded once per generation, so polling does not grow the history.

Replaying

A replay is not a retry. A retry keeps the work id and mints a new run id, and the queue does that on its own for a repeatable failure. A replay is a new authorization: it creates a new work item carrying replay_of, your reason, your identity rather than the original requester’s, and the original input. Terminal history is never rewritten. target_revision is inherited from the original unless you name one. Running against a different revision has to be explicit, and an unrecognised field in the body is a 400 rather than a silently ignored typo. Two refusals, both 409 with an explanation rather than an opaque failure:
  • The original has not finished. Replaying live work would start a second run of the same input. Cancel it first.
  • The payload is gone. A webhook-sourced replay needs the raw body the delivery ledger kept, and that body has its own retention. Once it expires the delivery record remains — so “we received it, we can no longer replay it” stays distinguishable from “we never saw it” — but the replay is unavailable and the response says so, naming the expiry. Ask the sender to deliver it again.
Check raw_body_available on the delivery before offering a replay control.

Webhook deliveries

Every inbound webhook is recorded before anything is dispatched, including the ones the filter rejected: an ignored delivery keeps its filter_reason and a null work_id, so a ping or a filtered event is auditable rather than invisible. Delivery identity is (workspace, endpoint, source delivery id) — never the signature, which changes with a fresh timestamp for the same logical delivery. Passing ?endpoint_id= and ?source_delivery_id= asks that identity question and answers with a page of zero or one. ?source_delivery_id= on its own is a 400: a sender’s delivery id is only unique within an endpoint. The raw payload is never returned. raw_body_available says whether it is still held (and therefore whether a replay can work), raw_body_expires_at says until when, and body_sha256 plus body_bytes identify it without publishing whatever the sender put inside.

CLI

Everything above has a command: see crewship work.

Resolve reconciliation

Example request body:
Only needs_reconciliation at the supplied generation can resolve, to succeeded, failed, or cancelled. Missing evidence returns 400; a stale generation or incompatible state returns 409. Success returns the updated work item and releases its ledger slots. The authenticated operator and reason are recorded. runtime_stopped is an explicit operator attestation, not a probe or a request to kill the process. Resolve does not start another attempt. Routine webhook receipts have no work-item controls. On upgrade, old unclaimed routine work records are preserved in needs_reconciliation with their original pipeline run identity. Inspect that pipeline run, then resolve the historical work item. Cancel and replay for these records return 409; use the pipeline API for any execution action.