# 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`](/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.                              |

```json
{
    "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

```text
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.
