Workbenches is an open standard. Read why
Docs Event protocol

The Workbench standard

Event protocol

Every Workbench run has an ordered stream of normalized events, the same for every runner. This is the integration boundary for hosts: wb run --json, wb resume --json, and wb attach --json write it as NDJSON, one complete event per line, and the workbenches.dev API stores and serves the same events for cloud runs.

The protocol is protocol: 0. It is versioned separately from the manifest spec number. Its normative schema is /schemas/events/v0/workbench-event.schema.json.

Envelope#

Every event has these fields, and only these:

Field Type Meaning
protocol 0 The event protocol version.
run_id string The run, matching ^wb_[a-z0-9]{20,64}$.
sequence integer, at least 1 Monotonically increasing within one run.
timestamp date-time string When the event was recorded.
type string One of the 33 types below.
runner string The runner that produced it, such as opencode.
data object Type-specific data.
{
    "protocol": 0,
    "run_id": "wb_d8m99qyw2e4qqhitfff63f1n",
    "sequence": 7,
    "timestamp": "2026-10-28T14:05:41.204Z",
    "type": "output.text",
    "runner": "opencode",
    "data": { "text": "Reading the migration." }
}

Sequences belong to one run. A cursor such as wb wait --after is only meaningful with the run it came from.

Event types#

run.started       run.ready
repository.preparing  repository.ready  repository.checks
delivery.started  delivery.completed  delivery.failed
authentication.requested  authentication.completed
turn.started      turn.completed
output.text
tool.started      tool.completed
file.changed
plan.updated
input.requested   input.accepted
input.queued      input.delivered      input.rejected
question.requested    question.answered    question.rejected
usage.updated
outcome.available     outcome.failed
preview.available
run.completed     run.failed     run.cancelled
runner.event

Exactly one of run.completed, run.failed, or run.cancelled ends the stream.

Data by type#

The schema constrains data for these types. Fields marked optional may be absent.

Type Data
repository.preparing, repository.ready repository (string), revision (40 character commit).
output.text text, optional id.
tool.started, tool.completed id, name; optional title, target, description, exit_code, status (completed or failed), duration_ms, outcome_id, error_code, message.
plan.updated items (each text and status: pending, in_progress, completed, or cancelled), completed, total.
usage.updated kind (delta); optional total_tokens, input_tokens, output_tokens, reasoning_tokens, cache_read_tokens, cache_write_tokens, cost_usd.
question.requested id, questions (each question, optional header, options, multiple, custom).
question.answered, question.rejected id, optional answer_count.
outcome.available outcome_id, completeness (complete or partial), application_state (pending, present, or applied), changesets, artifacts, links, warnings (counts); optional turn_index, summary.
outcome.failed message, turn_index.
preview.available name, port, protocol (http), optional url.
run.completed, run.cancelled Optional duration_ms and infrastructure.
run.failed message; optional duration_ms and infrastructure.
runner.event native_type.
repository.checks, delivery.* Reserved in protocol 0. The reference engine does not emit them; workbenches.dev emits delivery.* when a repository run pushes its branch, with the schema's delivery fields.

Turns#

A run may contain several turns. turn.completed means the runner finished one response and may accept another input; it does not end the run.

A turn that ends with reason: "length" hit the model's output limit, so its last tool call did not run. A run with no person attached gets one recovery turn, whose turn.started carries recovery: "length". A run whose latest turn still ends on length when it closes reports run.failed with code: "output_truncated" and a partial outcome.

Tools#

Tool events carry a stable call ID and the native tool name, and may carry a safe display title, target, short description, duration, and normalized failure. They never carry file contents or tool output.

A shell tool's target is its command, scrubbed before it leaves the adapter: values of environment assignments, secret-named flags, authorization headers, URL user information, and secret-named fields in headers, query strings, and bodies become [REDACTED], as do recognizable key formats and long opaque tokens. Heredoc bodies become a line count, control characters are removed, and the command is bounded to 500 characters on one line. Pattern scrubbing cannot recognize every credential, so a host that injects credentials should also redact their exact values before storing events.

exit_code is reported when the runner's tool metadata includes it. The OpenCode adapter passes it through; the Claude Code and Pi adapters report only a failed status.

Plans#

plan.updated carries the agent's whole plan, never a diff, whenever the adapter translates one, such as after a native todo tool call. items holds up to 50 entries. total excludes cancelled items. The plan can grow as the agent finds work, so expect total to change. Runners whose adapter does not translate a plan emit no plan events.

Inputs#

input.accepted means the host accepted the control request. input.queued means the input is waiting in the host or runner. input.delivered means it left that queue for the runner's active execution. Receipts and input events carry request IDs and dispositions, never prompt, image, or answer contents.

A runner can withdraw a permission or question it no longer needs. A withdrawn permission is recorded as input.rejected and a withdrawn question as question.rejected, each with code: "request_withdrawn".

Usage#

Usage is reported at native step completion, not token by token. Costs are provider-reported model costs. Descendant sessions a runner reports contribute their usage and tool activity to the owning run, with native_session_id on child activity.

Infrastructure#

Remote runtimes may attach infrastructure to the terminal event: provider, duration_ms, optional maximum_duration_ms, optional resources (cpu_count, memory_mb), and cost, an estimated or unavailable USD amount. Model tokens and model cost stay exclusive to usage.updated; clients must not combine the two silently.

Previews#

preview.available reports a server the agent started and announced with wbx preview <port> --name <name>. name matches ^[a-z0-9][a-z0-9-]{0,39}$, port is 1024 to 65535 and not one the engine uses, and a run has at most 8 previews. On the local runtime url is the loopback address http://127.0.0.1:<port>/. On Daytona the event has no url, because the port is reached through a signed URL, which is a credential.

Unknown native events#

Adapters translate events whose meaning is known and emit a minimal runner.event with only the native type for anything else. Portable events never copy arbitrary native payloads, which can contain reasoning, credentials, provider metadata, or complete tool results.

Large events#

The reference engine stores an event larger than 4 MiB serialized with its data replaced by { "truncated": true, "original_bytes": <n>, "limit_bytes": 4194304 }, keeping its envelope and sequence.

Compatibility#

Within protocol: 0, minor versions may add event types and optional data fields; existing types and fields keep their meaning. Clients must ignore event types and fields they do not recognize. The event stream is part of the 1.0 stability promise.