JSONL stream
The JSONL reporter writes one JSON object per line, a complete event stream of the run. It is the richest of the four reporters: every step and every UI action carries its full request, response, and timing data.
tales test ./e2e/pass --report-jsonl ./reports/events.jsonlThe file is written after the suite completes (not incrementally). The parent directory is created if missing.
Event types
Section titled “Event types”Each line carries a type field. There are three:
type | Emitted when |
|---|---|
scenario | Once per scenario, summarising its final status, duration, and (when failed/skipped) error / skip reason. |
step | Once per step. Includes a phase field set to "step" for main-flow steps and "teardown" for teardown steps. Retried steps surface their attempt count via attempts. |
action | Once per UI action for mobile steps that recorded actions (any --capture-screenshots mode other than none). Emitted immediately after the parent step event. |
There is no separate suite_start / suite_end envelope. Scenario aggregation is up to the consumer (jq -s 'group_by(.scenario)' or similar).
Example
Section titled “Example”{"type":"scenario","scenario":"Create blog post","file":"e2e/pass/blog.tales","status":"pass","duration_ms":842,"seed":1234}{"type":"step","phase":"step","scenario":"Create blog post","step":"create_user","provider":"http","status":"pass","status_code":201,"duration_ms":312,"seed":1234,"file":"e2e/pass/blog.tales","request":{"method":"POST","url":"http://localhost:1337/users"},"response":{"status_code":201,"json":{"id":"u_abc"}}}{"type":"step","phase":"step","scenario":"Create blog post","step":"create_post","provider":"http","status":"pass","status_code":201,"duration_ms":418,"seed":1234,"file":"e2e/pass/blog.tales"}{"type":"step","phase":"teardown","scenario":"Create blog post","step":"delete_user","provider":"http","status":"pass","status_code":204,"duration_ms":112,"seed":1234,"file":"e2e/pass/blog.tales"}The order is scenario event first, then its step events (main flow, then teardown) interleaved with any action events.
Fields you can rely on
Section titled “Fields you can rely on”scenario event
Section titled “scenario event”| Field | Type | Notes |
|---|---|---|
type | string | Always "scenario". |
scenario | string | Scenario name. |
file | string | Path to the .tales file that defines the scenario. |
status | string | pass, fail, skipped. |
duration_ms | number | End-to-end wall-clock duration of the scenario. |
seed | number | The --seed value used for this run. |
error | object | When failed: kind, path, want, got, message. |
skip_reason | string | Present only when status="skipped". Omitted otherwise. |
step event
Section titled “step event”| Field | Type | Notes |
|---|---|---|
type | string | Always "step". |
phase | string | "step" for main-flow steps; "teardown" for teardown steps. |
scenario | string | Scenario name. |
step | string | Step name. |
provider | string | http, sql, mobile, keyword. |
status | string | pass, fail, skipped. |
duration_ms | number | Step duration including retries. |
seed | number | The --seed value used for this run. |
file | string | Path to the .tales file that defines the step. |
status_code | number | HTTP status code, when applicable. Omitted for non-HTTP steps. |
attempts | number | Total attempts run, present only when greater than 1 (the step retried). A single-attempt step omits the field entirely. |
skip_reason | string | Present only when status="skipped". Omitted otherwise. |
request | object | Provider-specific request shape. Omitted when empty. |
response | object | Provider-specific response shape. Omitted when empty. |
actions | array | Mobile: per-action records, mirrored at the request level. |
artifacts | object | Mobile: paths to screenshot / hierarchy files. Omitted when empty. |
error | object | When failed: kind, path, want, got, message. |
action event (mobile only)
Section titled “action event (mobile only)”One per UI action, emitted after the step event when step.actions is populated. Empty action arrays produce byte-identical JSONL to the pre-action-stream format, so legacy consumers keep working.
{ "type": "action", "scenario": "iOS register", "step": "fill_form", "index": 0, "kind": "input_text", "label": "register.email", "status": "pass", "duration_ms": 42, "screenshot": "build/artifacts/mobile/.../screenshot.png", "hierarchy": "build/artifacts/mobile/.../hierarchy.json"}Secure values are masked to "***" at one boundary inside the mobile provider, see Visual HTML report.
DSN and secret masking
Section titled “DSN and secret masking”The SQL provider scrubs DSNs from every event before serialisation. Only the connection name, driver alias, SQL text, and arg count appear, never the connection string. See SQL provider for the full rules.
auth.basic.password is masked in HTTP request shapes. Other secrets injected via headers are not masked, capture them from upstream responses if you need masking.
Consuming the stream
Section titled “Consuming the stream”With jq
Section titled “With jq”# Count failuresjq -c 'select(.type=="step" and .status=="fail")' events.jsonl | wc -l
# All failure messages with their step pathjq -c 'select(.type=="step" and .status=="fail") | {scenario, step, msg:.failure.message}' events.jsonl
# Total wall-clock time across scenarios (parallelism aware)jq -s 'map(select(.type=="scenario") | .duration_ms) | add' events.jsonl
# All retried steps with their attempt countjq -c 'select(.type=="step" and .attempts) | {scenario, step, attempts}' events.jsonlIn a log pipeline
Section titled “In a log pipeline”JSONL plays nicely with Loki, Elasticsearch, BigQuery, Snowflake, Datadog logs. Ingest the file once at suite end and query at will.