Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Event schema — the run record for machine consumers

.proef-runs/<run-id>/events.jsonl is the run record (ADR-0008): one JSON object per line, in emission order, no second format. proef explain, diff, flaky and the console tree derive from this stream; --junit and the GitHub summary are built from the same run’s in-memory outcome (identical content, different plumbing — the stream stays the only persisted format; the one exception is a --rerun’s JUnit, whose carried base scenarios are reconstructed from the base record — see rerun_of below). Consume it with jq, a log shipper, or anything line-oriented.

Two derived sidecars sit beside the record and are not records: timings.json (per-scenario durations, re-derivable from the stream, read back by --shard-weights) and inputs.json (the run’s input fingerprint — a hash of proef’s own inputs, proef flaky’s equivalence class, ADR-0020 amendment; absent on pre-0.18 records). Neither adds a variant or a field to the stream.

Wire shape: serde-tagged with "event", snake_case names. The first line is always run_started and declares schema (currently 1); the last is run_finished. The schema is additive-only: new variants and new optional fields may appear, existing fields never change meaning or vanish. Consumers must ignore unknown variants and unknown fields.

Truncated records. A record whose last line is not run_finished is a run that did not finish — a hard interrupt (second signal, exit 130), a SIGKILL, or a crash. (A single SIGTERM/SIGHUP is a graceful cancellation: the record closes normally with run_finished + cancelled.) Treat it as partial: the scenarios present did happen, but the totals were never written and no verdict was reached. explain, report and diff classify these and say “run incomplete” rather than reporting a total they cannot know. Consumers should do the same rather than inferring zero.

One exception to “fields never change meaning”, recorded rather than hidden. run_finished’s passed/failed/skipped counted every phase before 0.6.0; from 0.6.0 they are the main-suite verdict and exclude [run] setup/teardown (ADR-0014), so those totals agree with the exit code, --format json/TAP, and the summary line. One deliberate divergence: a failed phase additionally appears in JUnit as its own suite (a gated pipeline must see the failure), so JUnit’s per-case counts can exceed these totals on a run whose setup or teardown broke. A pre-0.6.0 record read by a current consumer reports the older meaning.

Variants

run_started — head of every stream. schema (u32) · run_id (string — uuid-v7 by default, but --run-id passes any name through verbatim, so consumers must treat it as opaque).

scenario_started — scenario (string) · file (string) · timestamp_ms (u64, run-relative ms — only present with injected timing) · worker (u64, 0-based worker index — only present with injected timing). The timing pair is stamped at the CLI sink on the worker thread (the sans-IO core leaves it absent), and powers the HTML report timeline (ADR-0015).

batch_started — a contiguous same-engine step batch was dispatched. scenario · engine (e.g. "hurl") · steps (count).

entry_running — live progress: one event per execution attempt of an artifact entry, retries included. scenario · engine · entry (0-based ordinal within the scenario’s artifact) · retry (0 = first attempt).

step_finished — scenario · engine · step ({file, line, text}, the authored feature anchor) · status (passed | failed | skipped | warned) · attempts (u32) · duration_ms (u64) · captures (capture names only — never values) · fragment (file.hurl#name of the named fragment the step ran, ADR-0018 — only present for a ref: step, so an inline hurl: block and every pre-fragment record omit the key entirely) · label (the pack step’s authored name:, resolved — only present when the step has one. One feature sentence commonly lowers to several engine steps, and they share a step anchor exactly; the label is what tells them apart, and it is the same text the emitter writes into the artifact’s entry comment) · detail (string, only present on failures/warnings/skips-with-reason) · attempt_details (array of strings — the messages from earlier, failed attempts of a step that ultimately passed; only present for a flaky pass, feeds JUnit <flakyFailure>) · reproduce_hint (string — the redacted curl of the failing request, the same line the console prints as reproduce:; only present on failed steps, absent in every pre-field stream, masked at the sink boundary like detail).

scenario_finished — scenario · file (feature path — with scenario, the run-wide identity: names are unique only within one file; absent in records that predate the field) · status · timestamp_ms (u64, run-relative end ms — only present with injected timing) · worker (u64, in the schema but never populated by proef’s own writer today — it is emitted from the main dispatcher thread, not the scenario’s worker, so consumers must accept the field without expecting it — ADR-0015).

env / metadata / shuffled — on run_started, all additive and absent when unset. env is the active --env/PROEF_ENV profile name; metadata is the explicit user-supplied map (--meta k=v, [meta], [env.<name>.meta] — proef never harvests: no git, no hostname, no CI env sniffing; ADR-0020); shuffled says the order was re-dealt (the permutation is seeded by run_id, so the pair reproduces it). Keys and values pass the sink-boundary mask like every text field.

rerun_of — on run_started, optional (additive; absent = not a rerun). The base record’s run_id a --rerun re-ran failures from. The rerun’s JUnit carries the base’s not-re-run suite scenarios as ordinary testcases, and report overlays the base for a whole-suite page — composition over records, the record files themselves never merge (ADR-0008). Totals and the exit code stay the rerun’s own (ADR-0014).

tags — on scenario_finished, optional (additive; absent when the scenario carries none and in every pre-field stream). The accumulated tags (feature → rule → scenario → examples, deduped, authored order, @ stripped). On the finished event only: the cancel-skip path emits no scenario_started, and per-tag skip counts need every scenario.

exclusive — on scenario_started, optional bool (absent = false, which is what every pre-field record meant). The scenario ran with the pool to itself ([run] exclusive-tags) — the bool the scheduler itself read, recorded for timeline post-mortems (R11-6).

reason — on scenario_finished, optional (additive; absent in pre-field records and on every non-skipped scenario). Why the scenario is skipped: an authored skip carries the pasteable tag spelling (@skip / @skip:reason), which always begins with @; a mechanical skip (cancellation) carries proef-fixed prose, which never does. --rerun keys its re-queue decision on exactly that split (ADR-0019).

phase — on scenario_started/scenario_finished, optional. "setup" or "teardown" when the scenario belongs to a [run] lifecycle phase, absent for an ordinary suite scenario. Consumers should use it rather than inferring phase membership from the feature path: run_finished’s totals exclude phases (ADR-0014), so a consumer that counts every scenario_finished will not match them. Added additively — records without it have no phases, which is what they had.

run_finished — tail. passed · failed · skipped — the main-suite verdict: scenario counts for the primary suite only (ADR-0014). [run] setup/teardown scenarios still appear as their own scenario_started/ scenario_finished events earlier in the stream, but are excluded from these totals, so they agree with the console summary: line, proef explain, proef report’s HTML headline, --format json, TAP, the SLA gate, and the exit code — JUnit agrees too except that a failed phase rides in as its own suite (see above) · cancelled (bool, only present when true).

Example stream

{"event":"run_started","schema":1,"run_id":"019f…"}
{"event":"scenario_started","scenario":"reference","file":"suite/case.feature","timestamp_ms":0,"worker":0}
{"event":"batch_started","scenario":"reference","engine":"hurl","steps":2}
{"event":"entry_running","scenario":"reference","engine":"hurl","entry":0,"retry":0}
{"event":"step_finished","scenario":"reference","engine":"hurl","step":{"file":"suite/case.feature","line":4,"text":"the cookie session is exercised"},"status":"passed","attempts":1,"duration_ms":12,"captures":[]}
{"event":"step_finished","scenario":"reference","engine":"hurl","step":{"file":"suite/case.feature","line":5,"text":"the response status is 200"},"status":"passed","attempts":1,"duration_ms":0,"captures":[]}
{"event":"scenario_finished","scenario":"reference","file":"suite/case.feature","status":"passed","timestamp_ms":12}
{"event":"run_finished","passed":1,"failed":0,"skipped":0}

Guarantees

  • Secrets never appear. Redaction applies once at the sink boundary before any reporter (property-tested); captures carries names only.
  • Order: events of one scenario are internally ordered; scenarios running in parallel interleave. Group by (scenario, file) before assuming sequence across the stream — scenario alone collides when two feature files reuse a name.
  • Flake-safe assertions: assert on attempts counts and normalized event order, never wall-clock (duration_ms is engine-measured and varies).

Recipes

jq -r 'select(.event=="step_finished" and .status=="failed") | "\(.step.file):\(.step.line) \(.detail)"' events.jsonl
jq -r 'select(.event=="run_finished")' events.jsonl          # the suite verdict (setup/teardown excluded)
jq -r 'select(.event=="entry_running") | .retry' events.jsonl | sort | uniq -c   # retry pressure
# which fragment files a failing run actually exercised (ADR-0018); `// empty`
# drops the inline steps, which carry no `fragment` key at all
jq -r 'select(.event=="step_finished") | .fragment // empty' events.jsonl | sort | uniq -c