Reference
vero.plan/v1 reference
A plan is one YAML file: one run, one target. vero plan plan.yaml checks it and sends nothing
to the target; vero run plan.yaml runs it. This reference describes the format vero implements.
Start with Getting started if you need a source build and a first
runnable plan.
This page is the shape of a plan: the top level, tasks and the step envelope. Each step kind has its own page under Steps, and Templates, Assertions, the command line and states and exit codes have theirs. A test in the repository compares every key table on these pages with the loader's types, so a key the loader accepts and the pages do not list, or the other way round, fails the build.
apiVersion: vero.plan/v1
kind: Plan
metadata: { name: orders-api }
env:
baseUrl: ${VERO_BASE_URL}
apiKey: { secretRef: ORDERS_API_KEY }
lifecycle: { strategy: unique }
tasks:
- name: login
steps:
- http: { method: POST, url: "{{ env.baseUrl }}/auth/token", json: { key: "{{ env.apiKey }}" } }
assert: [status == 200, body.token != nil]
results: { token: body.token }
- name: create-order
steps:
- http:
method: POST
url: "{{ env.baseUrl }}/orders"
headers: { Authorization: "Bearer {{ tasks.login.results.token }}" }
json: { sku: "sku-{{ run.id }}", qty: 2 }
assert: [status == 201, body.order.status == "pending"]
results: { orderId: body.order.id }
create-order never says it runs after login: the reference to tasks.login.results.token is
the declaration. vero plan --graph shows every edge and where it came from.
The loader is hostile
Everything the loader can reject, it rejects before the first socket opens, and a plan that fails to load sends nothing (exit 64). In order:
- YAML: every key must be known (a misspelled key is an error with its line and a suggestion),
no key may appear twice at any depth, and a value in a string position must be a string.
0755,01310and1.10unquoted are refused with what YAML would have made of them (0755 decodes as 493; quote it: "0755"), never coerced. Merge keys (<<) are refused. - Templates parse, and every task, result and env key they name exists.
- The task graph has no cycle; a cycle is refused naming every task that could not run.
- Every assertion, guard and result compiles against its step's environment.
- Every resource in
holdsis declared inresources. - Every
secretRefresolves from the environment and is at least 4 bytes.
Paths in errors name tasks by name: tasks.create-order.steps[0].http.headers.Authorization.
Lint warnings (an all with no count beside it, a unique plan that never uses run.id) are
printed by vero plan and vero run; --strict makes them errors.
Top level
| Key | Type | Meaning |
|---|---|---|
apiVersion |
string | must be vero.plan/v1 |
kind |
string | must be Plan |
metadata |
object | see below |
env |
map | values templates and tasks can read as env.<key> |
resources |
list | names that tasks claim with holds |
lifecycle |
object | required: how a run avoids the last run's leftovers |
connections |
map | databases sql steps read and fixture steps write |
timeout |
duration | run timeout; vero run --timeout overrides it |
tasks |
list | the main graph; at least one |
finally |
list | teardown tasks, run after the main graph whatever happened |
| Key | Type | Meaning |
|---|---|---|
name |
string | the plan's name in reports |
env
Each value is a string, or { secretRef: NAME }, which reads NAME from the process environment
at load and is then scrubbed by value from every output (terminal, event log, JUnit, TAP), in plain,
URL-escaped, JSON-escaped and base64 forms. A string may use ${VAR} to read the process
environment at load (an unset variable is a load error) and {{ run.id }}; ${VAR} works here
and in a connection's dsn, nowhere else in a plan. ${VAR:-default} takes default when VAR is unset or empty, as a shell does; the
default runs to the first }, so it cannot hold a {{ }} template. Keys match [A-Za-z_][A-Za-z0-9_]*.
resources
| Key | Type | Meaning |
|---|---|---|
name |
string | any string without spaces, e.g. ratelimit/orders-api-key |
mode |
exclusive or shared |
the claim a bare name in holds makes; exclusive when absent |
lifecycle
| Key | Type | Meaning |
|---|---|---|
strategy |
unique, reset or isolated |
required, no default |
reset |
list of steps | with reset only: exec, http or fixture steps run in order before any task |
sharedTarget |
bool | prints a caveat that a run against a shared environment cannot be deterministic |
unique: the plan namespaces what it creates with run.id (a ULID per run); the loader warns if
nothing uses it. reset: if a reset step does not pass, no task starts, every task is not_run and
the exit is 3; each reset step needs a timeout, may not read task results, and without assertions
is held to status < 400 (http) or exitCode == 0 (exec).
isolated declares that each run gets its own target (a schema, a container, a tenant), brought
up by the plan's own tasks; vero starts no service for it.
connections
| Key | Type | Meaning |
|---|---|---|
driver |
sqlite or postgres |
|
dsn |
string or { secretRef } |
may use ${VAR}, ${VAR:-default}, {{ env.x }} and {{ run.id }}, never a task result |
writes |
bool | this connection is for fixture steps only, and sql steps may not use it |
A connection no step uses is a load error. SQLite is opened mode=ro (and waits up to 5 s on
a writer's lock); Postgres runs every step READ COMMITTED READ ONLY, checks
transaction_read_only inside the transaction, and vero plan/vero run warn when the
connection's role could write. A connection with writes: true is opened as the DSN says, and
its role is not checked: writing is what it is for.
Tasks
| Key | Type | Meaning |
|---|---|---|
name |
string | ^[a-z0-9][a-z0-9-]*$, unique across tasks and finally |
needs |
list of task names | explicit ordering with no data flowing |
holds |
list | resource claims: a name, or { name, mode } to override the resource's mode |
when |
list of expressions | all must hold or the task is skipped; reads env, tasks, run |
timeout |
duration | covers all the task's steps; no default beyond the steps' own |
cookies |
bool | a cookie jar for this task run only; no jar spans tasks |
tags |
list of strings | slow tasks are skipped unless --include-tag slow |
matrix |
list of maps of strings | run the task once per row, see below |
steps |
list | run in order; the task stops at the first step that does not pass |
A task with matrix becomes one task per row when the plan loads, named <task>-<key>:
- name: create-order
matrix:
- { key: small, sku: ABC-1, qty: "1" }
- { key: bulk, sku: ABC-1, qty: "500" }
steps:
- http: { method: POST, url: "{{ env.base }}/orders", json: { sku: "{{ matrix.sku }}", qty: "{{ matrix.qty }}" } }
assert: [status == 201]
Each row needs a key that follows the task-name rule. Values are literal strings: quote numbers,
and a {{ in a value is refused. There are at most 100 rows. {{ matrix.<field> }} is replaced as
text everywhere in the row's task, assertions and results included, and a field a row lacks is a
load error. Every row keeps the task's needs, holds, when and tags, so holds keeps rows
of one task apart too. Other tasks name a row, as in create-order-bulk. Naming the base task is a
load error that lists its rows, and vero plan --explain create-order lists them. The table is
never read from a file or built at run time: a plan that needs that needs a script step.
A task runs when every task it needs has passed and every resource it holds is free.
holds is exclusion, not ordering: two holders of one resource never overlap, in whichever order.
A when guard that reads tasks.X also creates an edge on X (derived:when in --graph). A
task whose upstream failed, errored, timed out or was skipped is blocked; one whose own guard is
false is skipped instead, even then. A task blocked by a skip makes the run exit 3, because
nothing is known about it; if it is optional too, give it the same guard so it skips in its own
right.
finally tasks have the same shape. They run after the main graph drains, under
--finally-timeout, even after a run timeout or a first SIGINT. A main task may not reference a
finally task. A finally task whose template reads a result its upstream never produced is
blocked; guard it with when (tasks.create-order.results.orderId != nil) to make it skipped
instead. An explicit needs on a main task orders nothing here: finally tasks run after every
main task has ended, whatever its state, so only the data edge decides.
Steps
| Key | Type | Meaning |
|---|---|---|
http |
object | an HTTP request |
graphql |
object | a GraphQL request, sent as an HTTP POST |
ws |
object | a WebSocket: send messages, collect what comes back |
grpc |
object | one gRPC call, unary or streaming |
sql |
object | a read-only query |
exec |
object | a process |
waitFor |
object | poll until ready |
script |
object | a vero.script/v1 child process |
fixture |
object | a committed write of reference data |
repeat |
object | http only: send the request count times |
timeout |
duration | the step's deadline, default 30 s, covering the whole exchange; a waitFor step has its own and refuses this one |
assert |
list of expressions | required (except waitFor): a step that asserts nothing is refused |
results |
map | name to expression, or to { secret: expression }; evaluated after the assertions and published when the task passes |
optional |
list of paths | paths --strict-paths may find absent, e.g. body.debug, body.items[*].qty |
Exactly one of http, graphql, ws, grpc, sql, exec, waitFor, script, fixture per step. A step may read a result an
earlier step of its own task produced ({{ tasks.<self>.results.x }}), which creates no edge. The
same result key in two steps of a task is an error. A result that evaluates to nil is absent.
A result written { secret: body.token } is a secret from the moment its step answers. vero
evaluates it before it writes any of the
step's output, whether the assertions hold or not, and every output after that shows
«redacted tasks.<task>.results.<name>» in its place: the step's own response, later request
headers, the event log, JUnit, TAP and the HTML report. Templates still render the real value, so
the server gets the token. A value shorter than 4 bytes can't be scrubbed safely: the step ends
errored and its exchange is not shown. A script step's result can't be secret, because its
result event is written as the child sends it.
- http: { method: POST, url: "{{ env.baseUrl }}/auth/token", json: { key: "{{ env.apiKey }}" } }
assert: [status == 200]
results:
token: { secret: body.token }
http
An HTTP request: method, URL, headers, query, one body of five kinds, optional redirects and
retries, and repeat for sending it several times. The http step.
graphql
A GraphQL query sent as an HTTP POST, parsed at load, with an implicit check that the response
carries no errors. The graphql step.
ws
A WebSocket: send text or JSON frames after the upgrade and collect what comes back until a count or a timeout. The ws step.
grpc
One gRPC call, unary or streaming, described by .proto files compiled at load; the server is
never asked for its schema. The grpc step.
sql
One read-only statement through a declared connection, bound parameters only, in a transaction that is rolled back. The sql step.
fixture
One committed write of reference data through a connection declared writes: true, labelled in
every output. The fixture step.
exec
A process from an argv list, no shell, in its own process group, with capped output. The exec step.
waitFor
Poll an HTTP request until a condition holds, with no assertions of its own. The waitFor step.
script
A vero.script/v1 child process in Node or Python, given an input and reporting steps and a
result over stdio. The script step.
Templates
{{ path }} works in string values and holds a path, never an expression: env.<key>,
tasks.<name>.results.<key>, run.id, and matrix.<field> inside a matrix task. ${VAR} reads
the process environment at load, in env values and connection DSNs only.
Templates lists every position and every rule.
Assertions
An assertion is an expr expression that must be a boolean, plus the
forms vero rewrites before compiling: responses count 4, exists body.x, duration literals,
matches schema, matches openapi, matches snapshot. Numbers compare exactly.
Assertions has the complete syntax, the names in scope for each step
kind, --strict-paths and the lints.
Running
vero plan checks a plan, prints its graph or explains one task; vero run runs it and exits
by the worst task state. Every flag is on the command line page, and the seven
terminal states with the exit codes they produce are on
States and exit codes.
This page is docs/content/plan-format.md in the repository.