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:

  1. 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, 01310 and 1.10 unquoted are refused with what YAML would have made of them (0755 decodes as 493; quote it: "0755"), never coerced. Merge keys (<<) are refused.
  2. Templates parse, and every task, result and env key they name exists.
  3. The task graph has no cycle; a cycle is refused naming every task that could not run.
  4. Every assertion, guard and result compiles against its step's environment.
  5. Every resource in holds is declared in resources.
  6. Every secretRef resolves 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.

verodocs