Contracts
vero.script/v1
The protocol between vero and a script step's child process (chapter 9.2). It is deliberately
dull: one JSON object in, newline-delimited JSON events out, stderr as logs. No RPC, no calls back
into vero, no shared memory. The child never sees the graph, cannot create tasks, and reaches
another task's results only through what the plan templated into input. That is what keeps the
graph static.
Running
vero starts <runtime> <entry> as an argv, with no shell, in its own process group, with the plan
file's directory as the working directory. The child's environment is PATH, HOME and LANG
from vero's own, when set, plus VERO_SCRIPT_TOKEN, a value unique to the step that vero uses
after the step to find a process that left the group and kept running. The runtime is the user's own interpreter found on
PATH: node for runtime: node, and python3, then python, for runtime: python. The binary
found and what its --version printed go into the run header and the run.start event. A plan
with a script step whose runtime is not on PATH fails to load (exit 64) naming the runtime and the
task; a plan with no script step never looks for one.
stdin: one message
vero writes exactly one line and closes stdin:
{"protocol":"vero.script/v1","input":{...},"deadline":"2026-09-05T12:00:00Z"}
| Field | Type | Meaning |
|---|---|---|
protocol |
string | always vero.script/v1 |
input |
object | the step's input, with every template rendered; {} when the step has none |
deadline |
string, RFC 3339 UTC | when vero will stop the process group; the step's deadline |
stdout: events, one JSON object per line
{"type":"log","level":"info","msg":"navigating to /checkout"}
{"type":"step","name":"fill address","status":"passed","durationMs":312}
{"type":"result","ok":true,"result":{"orderId":"01J8Z…","checkoutMs":3820}}
| Type | Fields | Meaning |
|---|---|---|
log |
level (string), msg (string) |
a line for the report and the event log |
step |
name (string), status (string), durationMs (number) |
something the script checked; see "unverified" below |
result |
ok (boolean, required), result (any JSON) |
the step's result; the last result event is the one used |
result.result is decoded like a response body: numbers are exact, and a key that appears twice is
an error. Assertions see it as result, and result.ok as ok; results: extract from it as from
any step.
What vero does with a stream that breaks the rules
- An unknown
type: recorded as awarnlog line naming the type and the line number. Never a failure, so a newer script library does not break an older vero. - A line that is not JSON, or has no
type: the step iserrored, naming the stdout line number. - A line longer than 1 MiB: the step is
errored, naming the line number. - A result event without
ok: the step iserrored. - stdout ends with no
resultevent: the step iserrored("the script's stdout ended without a result event"). - More than 1 MiB of
logevents: the rest are read and dropped, and the count of dropped events is reported.stepandresultevents are always kept. - A non-zero exit code: the step is
errored("the script exited with code 2"), even when aresultevent arrived first. Aresultwithok: falseand exit code 0 isfailed.
stderr
Captured, capped at 1 MiB like an exec step's output, and reported as log lines.
Unverified
Assertions inside a script are invisible to vero (chapter 9.4). A script that returns ok: true
and emitted zero step events is reported as passed (unverified): it counts as passed for the
exit code, and every report says it was not verified. Emitting one step event per thing checked is
what makes a script's pass a pass.
Secrets and cancellation
Every known secret value is scrubbed from stdout events and stderr before they reach any output;
a screenshot or any other file the script writes is not scrubbed. On the step's deadline, a run
timeout or a signal, vero signals the whole process group; a process that detached itself from the
group (for example with setsid) survives. vero gives it a second to exit on its own, as a browser
started in its own group does once its parent's pipe closes, and reports what is still running
after that as a process it could not account for.
This page is docs/content/contracts/script-v1.md in the repository.