Workbenches is an open standard. Read why
Docs Sessions

Running Workbenches

Sessions

Every execution belongs to one stable Workbench session. The first run shares its wb_... ID with the session. Each continuation creates a new internal run while the session ID stays fixed. A session stays locked to its original Workbench version, runner, model, runtime, workspace, and workspace bindings.

These commands let a person or a supervising agent drive a session without keeping a client attached.

Command Does
wb ps List active and resumable sessions.
wb attach Replay and follow a run. Read-only.
wb wait Block until the next boundary and print one snapshot. Read-only.
wb send Send input: to an idle session, as steering, or as a queued follow-up.
wb answer Answer a pending permission or question.
wb resume Continue a session with new input.
wb kill Cancel the active run.

The loop#

wb run project-core --task-file ./brief.md --detach --json
# {"session_id":"wb_...","run_id":"wb_...","input_id":"...","after_sequence":0}

wb wait wb_... --timeout 120 --json

wb send wb_... --task-file ./followup.md --json
# {"session_id":"wb_...","run_id":"wb_...","input_id":"...","after_sequence":42}

wb wait wb_... --run --after 42 --timeout 120 --json

Each command returns correlated session and run IDs. Sequence cursors belong to one run, so wait on the run_id and after_sequence returned by the command you just issued.

ps#

wb ps          # active sessions and sessions with resumable context
wb ps --all    # all session history, including terminal one-shot runs
wb ps --json   # one record per NDJSON line

--json includes state, needs_input, pending requests, and project, the project's display name or null. A run that seems slow with nothing happening is almost always waiting on a permission: check needs_input.

attach#

wb attach wb_...
wb attach                 # the latest session
wb attach wb_... --json   # replay the stored events as NDJSON
wb attach wb_... --final

Attaching replays the latest run's stored events and follows new ones. It never takes control, starts model work, or keeps a runtime alive. If an observer exits while a turn is active, the turn and all accepted follow-ups finish before the background worker closes.

wait#

wb wait <session-or-run> [--run] [--after <sequence>] [--timeout <seconds>] [--json]

wait is a read-only observer. It prints one snapshot when it reaches the first of:

  • a completed turn after the cursor,
  • an idle boundary,
  • a pending permission, question, or authentication request,
  • a terminal state,
  • the timeout.
Option Effect
--run Treat the ID as an exact run. Without it, a session ID selects its latest run. The session and its first run share an ID, so use --run to pin the first run.
--after <sequence> Wait past this event sequence. Without it, observation starts at the beginning of the run.
--timeout <seconds> Stop observing after this long. The run is unchanged.
--json Print one JSON result. Never an event stream.

It returns the first completed turn after the cursor even if queued work starts immediately. Repeat with --after set to the returned sequence to see the next boundary. A timeout or interrupted wait never cancels the run, and waiting never attaches a controlling client, starts authentication, or keeps a runtime alive.

For an authoring run started by wb create, wait waits for package verification too.

States#

state in the result is one of:

State Meaning Exit code
starting The run is being prepared. 0
running A turn is active. 0
idle The session is open and waiting for input. 0
turn_completed A turn replied. The run may continue; cleanup has not necessarily finished. 0
completed The run finished. 0
needs_input A permission, question, or authentication request is pending. 2
failed The run failed, including a run with no authenticated route. 1
cancelled The run was cancelled. 130
timeout --timeout passed first. The run is unchanged. 124

wait itself exits 130 when it is interrupted. The full result shape is on JSON output.

Usage in a wait#

usage counts usage events strictly after --after through the returned sequence (kind: delta). usage_total counts the whole run through that sequence (kind: total). Advance the cursor before summing another wait's usage; waiting on the same cursor again repeats the same interval. Costs are provider-reported model costs, not billing or infrastructure charges.

send#

wb send wb_... "Check the rollback" --json
wb send wb_... "Focus on cleanup" --steer --json
wb send wb_... --task-file followup.txt --queue --json
wb send wb_... --stdin --json
Option Effect
[text], --task-file <file>, --stdin The input. Explicit alternatives, never implicit fallbacks.
--steer Change the active turn. Requires an active execution.
--queue Queue a FIFO follow-up for the next turn boundary.
--json Print one receipt.

An ordinary send rejects an active turn instead of silently queuing it. Sending to a closed resumable session starts a fresh run from its saved native context. send uses the inherited environment for credentials.

Steering support depends on the runner: OpenCode steers mid-turn, Claude Code at the next tool result, and Pi at its next model boundary. A receipt and the input lifecycle events show when the runner accepts and delivers the input; a receipt is not proof the runner consumed it.

answer#

wb answer wb_... <request-id> allow --json
wb answer wb_... <request-id> deny
wb answer wb_... <request-id> '[["First option"],["Second option"]]'
wb answer wb_... <request-id> --response-file answer.json
wb answer wb_... <request-id> --reject

answer resolves only a request ID that wait or ps currently reports. For a permission, allow grants it once and deny rejects it. For a question, pass the answer text, or a JSON array of string arrays for several questions or multi-select. --response-file and --stdin read the response; --reject rejects a pending question.

Authentication requests expose the runner's URL and instructions, never a channel for submitting a credential. Permissions and questions exist only when the runner has a native protocol for them: OpenCode and Claude Code do, Pi does not.

If the same paths prompt for permission again and again, the Workbench's runner permissions are missing entries. Report it to the author rather than approving everything.

resume#

wb resume wb_... "Review the latest change"
wb resume wb_... --task "Run the checks" --detach
wb resume wb_... --task "Continue" --allow-host-docker

resume continues a session with new input. It joins an active run's follow-up queue, or starts a linked run from saved native context when the previous one closed. It requires input: without it, Session wb_... needs new input.

It accepts the task options of run (--task, --task-file, --stdin), the output options (--detach, --json, --final, --color), --env-file, --env, --connection, and --allow-host-docker. A Workbench that declares host Docker access needs --allow-host-docker again on every resume.

resume uses the exact package and workspace recorded by the session. It is rejected if the package no longer matches the recorded name, version, runner, model, or workspace, or if the runner never reached a resumable state. Use wb send for agent-driven continuation.

kill#

wb kill wb_...

Cooperatively cancels the active run. The worker terminates the runner, emits run.cancelled, and marks the run cancelled. The session and its resumable context remain.

clean#

wb clean                              # preview terminal history older than 30 days
wb clean --older-than 7d --apply
wb clean --older-than 0s --include-sessions --apply
wb clean --json

wb clean never removes anything without --apply. The default selects terminal, non-resumable sessions and obsolete run history older than 30 days. Durations look like 12h, 7d, or 4w. Active runs are never eligible. Resumable context and its latest run are protected unless --include-sessions is also passed. Repository sessions keep every earlier attempt's outcomes until the session is removed. --json returns the preview or result with byte counts and protected resources.

It also finds managed Docker containers and E2B sandboxes whose run is terminal or missing. It never removes images, build caches, E2B templates, runner credential volumes, or unrelated resources.

Where sessions live#

Sessions, runs, events, and projects are rows in one SQLite database, $WORKBENCH_HOME/wb.db (normally ~/.workbench/wb.db). WORKBENCH_HOME must be on a local disk; the database does not work on network filesystems such as NFS. A run's native runner state, managed checkout, and scratch live under $WORKBENCH_HOME/files/.