FeaturesSessions

Status and lifecycle

A session's status and admission_state, turn states, interrupt, and attention

The session detail (GET /v1/sessions/{id}) has two states. status is what the execution is doing now, and admission_state is whether the session accepts new input. A turn has its own state, and you judge whether it has finished by the turn state alone. Look at the session created in Start (created and sessionId in the SDK, SID in the HTTP API).

import { waitForTurn } from "@werft-ai/sdk";

await waitForTurn(werft, sessionId, created.turn_id);
const { status, admission_state, current_turn_id, revision, attention } =
  await werft.sessions.get(sessionId);
console.log({ status, admission_state, current_turn_id, revision, attention });
{
  status: 'idle',
  admission_state: 'active',
  current_turn_id: null,
  revision: 0,
  attention: null
}

revision goes up when a control such as pause, resume, or close changes the session. It does not go up when a turn finishes. Send the current value as expected_revision in control requests.

status

ValueMeaning
queuedThere is input and it is waiting for a worker
runningA worker is running a turn
needs_inputThe running turn is waiting for a person to answer a permission request or question
idleNo turn to run. The next message is accepted
stoppedExecution has stopped (after interrupt, terminate, pause, or close)
failedExecution failed. The reason is in attention

admission_state

ValueMeaning
activeAccepts messages and controls
pausing → pausedPausing, then the state where a checkpoint is saved and the worker is down. Messages get 409 SESSION_PAUSED
resumingResuming. A new worker restores from the checkpoint
stopping → stoppedTerminating, then stopped
recovery_requiredA turn with an unknown outcome remains, and a person must make a recovery decision (Close)
closing → closedClosing, then closed. This cannot be undone

Turn states

for await (const turn of werft.sessions.turns.list(sessionId)) {
  console.log(turn.turn_id, turn.status);
}

queued, running, and needs_input are in progress; completed, failed, interrupted, cancelled, and outcome_unknown are finished states. completed means the engine ended the turn normally, not that the requested work was done. Decide whether the work is done by reading the assistant and tool_result events. The result event is not a completion verdict either.

interrupt

This stops only the one running turn. The worker stays and accepts the next message. The script below delays its reply by 5 minutes, so the turn stays running in the meantime. The delay works only with the development server's fake model; with a real model, try it on a turn given long-running work.

const sent = await werft.sessions.messages.send(sessionId, {
  message:
    'GATE-SPEC {"id":"slow","steps":[],"final":"too late","finalDelayMs":300000}',
});
while ((await werft.sessions.turns.get(sessionId, sent.turn_id)).status !== "running") {
  await new Promise((resolve) => setTimeout(resolve, 1000));
}
const accepted = await werft.sessions.interrupt(sessionId, {
  target_turn_id: sent.turn_id,
});
console.log(accepted.receipt_status);
const turn = await waitForTurn(werft, sessionId, sent.turn_id);
console.log(`turn ${turn.turn_id}: ${turn.status}`);
accepted
turn 2: interrupted

A turn that has not started yet gets 409 TURN_NOT_STARTED, and a turn that has already finished gets a successful no-op (no_op: true). The scope is sessions:control.

attention

When the session is waiting for a person, attention carries a code and a reason. Normally it is null.

codeMeaning
PAUSE_BLOCKED, CLOSE_BLOCKEDPause or close could not drain for over 60 seconds. The reason is long_turn, pending_request, and so on
BUDGET_EXCEEDEDThe session cost or token limit was exceeded
TRANSCRIPT_LIMITThe transcript is near or over its limit. Time to move to a new session
STARTUP_FAILED, RESTORE_FAILEDThe worker cannot start or restore a checkpoint
CATALOG_MISMATCHAn operator changed the profile or repository settings after the session was created
PROFILE_BLOCKEDAn owner blocked the session's profile (reason_code: blocked) or its base (base_blocked); profile_id names it. Until it is unblocked, new messages are refused with 409 PROFILE_BLOCKED
CONTEXT_GAPA recovery decision dropped part of the earlier conversation

Usage

const usage = await werft.sessions.usage(sessionId);
console.log(usage.cost.amount_usd, usage.cost.complete, usage.budget_exceeded);
0.000063 true false

The cost is an estimate computed from a price list, not a billed amount. If complete is false, the total is not settled yet. Either there is an open turn (open_turn_count), or some finished turn has no reported cost (unreported_turn_count). In the latter case it stays false even after all turns finish.

To add up every session you started in a period, use GET /v1/usage/summary. The SDK has no method for this total or for GET /v1/limits?scope=me, so read them with the HTTP API. The SDK's limits.get() reads the installation-wide limits (GET /v1/limits), which only the signed-in workspace owner can read, so calling it with an API key gets 403. Reference: GET /v1/usage/summary, GET /v1/limits since is a date (that day's 00:00 UTC) or a timestamp with an offset; without until the period runs until now.

curl -sS "$API/v1/usage/summary?scope=me&since=2026-10-01" -H "Authorization: Bearer $KEY" \
  | jq '{session_count, cost_usd, cost_complete, token_count, run_seconds}'

It counts the sessions created in the period, and cost_usd is the sum of their /usage cost.amount_usd. If any of them has complete: false, cost_complete is false too. A personal key adds up the sessions of the person who created it. scope=installation (every owner's sessions) and GET /v1/limits are readable only by the signed-in workspace owner; any other member and every API key get 403. scope=agent&agent_id=… adds up the sessions started from any release of that agent, whoever started them. The workspace's owners and admins and the agent's admins read it; any other member and every API key get 403, and an agent of another workspace, or one that does not exist, is 404 for everyone. It cannot be combined with group_by.

usage.outputs in GET /v1/limits is the bytes session outputs (files under out/) take up in the object store (used_bytes). An output a later checkpoint carries over from an earlier one counts once, a file too large to upload does not count, and the figure drops when checkpoint cleanup deletes an object. updated_at is when the figure last changed, null if it never has. It is separate from usage.storage (input messages) and is not part of storage_limit_bytes enforcement. While a checkpoint made before outputs were recorded is still kept, its outputs are left out and complete is false.

To split it by agent or by user, add group_by=agent or group_by=user. The response then carries groups[{id, name, session_count, run_seconds, cost_usd, cost_complete}], and the groups' session_count, cost_usd and run_seconds add up to the totals above. Sessions without an agent, and sessions started by a key no person created, are the group whose id is null. name is the agent's name or the user's display name, never an email; under scope=me a user other than the caller has a null name. To split it by profile, use group_by=profile. Sessions are grouped by their current profile (sessions.profile_id), so a session that changed profile counts whole under the one it has now. Profiles have no display name, so both id and name are the profile id, and sessions without a profile are the group whose id is null.

GET /v1/limits?scope=me is readable by any signed-in member. It gives the four per-session limits alone (queued_input_limit_per_session, max_turn_seconds, session_cost_limit_usd, session_token_limit) with no usage. API keys get 403.

curl -sS "$API/v1/usage/summary?scope=me&since=2026-10-01&group_by=agent" -H "Authorization: Bearer $KEY" \
  | jq '.groups[] | {name, session_count, cost_usd}'

Next steps

On this page