# Watching a collection run

> For the complete documentation index, see [llms.txt](/llms.txt)

An agent that starts a collection in the background —

```sh
npx @unotest/web collection smoke &
```

— sees the CLI's per-scenario lines only when the process ends, because a
piped stdout is block-buffered. The run's own journal is the progress
source. It is written line by line as the run goes:

```
unotest/.runs/<yyyy>/<mm>/<dd>/col-<name>-<id>/steps.jsonl
```

`.runs` becomes `.runs.<env>` for a `--env <name>` run; the date shard is
UTC. `col-<name>-` is the collection run's id prefix and the `<id>` tail is a
timestamp plus random hex, so take the newest directory with that prefix once
the run has started.

## The events

One JSON object per line, four kinds:

| `kind` | Fields | When |
| --- | --- | --- |
| `collection-run:started` | `runId`, `collection`, `scenarios[]` (in order) | before the first scenario |
| `collection-run:scenario-started` | `scenario`, `scenarioRunId` | each scenario takes a worker |
| `collection-run:scenario-finished` | `scenario`, `scenarioRunId`, `outcome` | `completed` / `failed` / `aborted` / `interrupted` |
| `collection-run:finished` | `outcome` | the run is over — nothing follows |

Every line also carries `t` (epoch ms). `scenarioRunId` names the scenario's
own run directory under the same `.runs` root, where its `steps.jsonl` and
[failure bundle](/concepts/failure-bundles/) live. A line that does not parse
is the one being written — retry on the next read, never treat it as the end.

```sh
tail -f unotest/.runs/2026/09/03/col-smoke-*/steps.jsonl \
  | jq -c 'select(.kind | startswith("collection-run:scenario"))
           | {kind, scenario, outcome}'
```

## A contract, not an implementation detail

The event types (`CollectionRunArtifact`) and the file name (`STEPS_FILE`)
are exported from `@unotest/protocol` under semver, and the box daemon reads
the same events for its metrics. Build on them.
