Reference

Templates

A template is {{ path }} inside a string value of the plan. Inside the braces is a path, never an expression: env.<key>, tasks.<name>.results.<key> or run.id. No filters, calls or arithmetic. The loader parses every template before anything runs, checks that every key and task it names exists, and derives the task graph from the task references it finds. That is why the syntax is this small: a reference the loader can read is an edge it can draw. See Plan format for the shape of a plan and Assertions for the expression language, which is a different thing.

  - 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]

Where a template works

Every string the plan sends is templated. Every string that names a file, a method or a literal the loader must read is not.

Step Templated Literal
http url, headers, query, form, every string inside json, body, multipart.fields method, bodyFile, a file part's path, filename and contentType
graphql url, headers, operationName, every string inside variables query, queryFile
ws url, headers, send[].text, every string inside send[].json
grpc target, metadata, every string inside message and send[] method, protos, importPaths
sql args query
fixture args statement
exec command, env, stdin, workdir
waitFor its http block, as above
script input, nested strings included entry
any file names in matches schema, matches snapshot and matches openapi; repeat.count; timeouts; task and resource names

Two more places take templates with a narrower grammar. An env value may reference run.id and nothing else, so tenant: "t-{{ run.id }}" is fine and tenant: "{{ env.baseUrl }}" is a load error. A connection's dsn may reference env.<key> and run.id, never a task result, because a connection belongs to the whole plan and opens before any task has published anything.

A template in a JSON position renders as a JSON string, whatever the result was. { qty: "{{ tasks.cart.results.qty }}" } sends "qty": "2". Write a number as a literal when the server wants a number.

Syntax

Whitespace inside the braces is trimmed, so {{env.baseUrl}} and {{ env.baseUrl }} are the same reference. The loader reports a template that does not parse at the string's plan path and the column of the {{:

Written Load error
{{ env.baseUrl /healthz unclosed {{; a reference is written {{ env.key }}, {{ tasks.name.results.key }} or {{ run.id }}
{{ }} empty {{ }}; write a path such as {{ env.baseUrl }}
{{ page }} {{ page }} is not a path; a template holds only env.<key>, tasks.<name>.results.<key> or run.id
{{ env.a | upper }} {{ env.a | upper }} is not a path; a template holds only env.<key>, tasks.<name>.results.<key> or run.id, with no filters, calls or operators
{{ env.a.b }} {{ env.a.b }}: an env reference is env.<key>
{{ tasks.login.token }} {{ tasks.login.token }}: a task reference is tasks.<name>.results.<key>
{{ run.started }} {{ run.started }}: the only run reference is run.id

Because {{ always opens a reference, a literal {{ cannot appear in a templated string. A GraphQL query is a literal for the same reason, and a {{ in it is refused at load.

Task names follow ^[a-z0-9][a-z0-9-]*$ and keys follow ^[A-Za-z_][A-Za-z0-9_]*$. The hyphen in tasks.create-order.results.orderId is part of the name, and the parser knows it.

What a reference must point at

A template that parses is then resolved against the plan. Each of these is a load error:

  • {{ env.apiKey }} when env has no apiKey: {{ env.apiKey }} names env key apiKey, which the plan's env block does not define (it has: baseUrl, tenant).
  • {{ tasks.logn.results.token }} when no task is called logn: names task logn, which does not exist (did you mean login?). When the name is a matrix task's base name, the error lists its rows instead: ; it is a matrix, so name one row: create-order-small, create-order-bulk.
  • {{ tasks.login.results.tokn }} when login declares no tokn: names result tokn, which task login does not declare (it has: token).
  • A step reading {{ tasks.<self>.results.x }} where no earlier step of the same task declares x: reads a result of its own task that no earlier step produces. Reading a result an earlier step of the same task produced is allowed and creates no edge.
  • A lifecycle.reset step reading any task: a reset step runs before any task, so it cannot read {{ tasks.login.results.token }}.
  • A main task referencing a finally task: main task create-order references finally task cleanup; finally tasks run after every main task has finished, so nothing in tasks can depend on them.

Every reference to another task's result is an edge in the graph, shown by vero plan --graph as derived:template with the plan path it came from. A template reference is the declaration of the dependency. needs is for ordering with no data flowing.

This is one run of vero plan over a plan with six template mistakes. The loader reports all of them at once, each with its line and plan path:

template-errors.yaml:6: env.tenant: an env value may reference only {{ run.id }}, not {{ env.baseUrl }}
template-errors.yaml:11: tasks.login.steps[0].http.json.key: {{ env.apiKey }} names env key apiKey, which the plan's env block does not define (it has: baseUrl, tenant)
template-errors.yaml:18: tasks.orders.steps[0].http.url: column 31: {{ page }} is not a path; a template holds only env.<key>, tasks.<name>.results.<key> or run.id
template-errors.yaml:19: tasks.orders.steps[0].http.headers.Authorization: {{ tasks.logn.results.token }} names task logn, which does not exist (did you mean login?)
template-errors.yaml:19: tasks.orders.steps[0].http.headers.X-Run: column 1: {{ run.started }}: the only run reference is run.id
template-errors.yaml:23: tasks.unclosed.steps[0].http.url: column 1: unclosed {{; a reference is written {{ env.key }}, {{ tasks.name.results.key }} or {{ run.id }}
template-errors.yaml: 6 load error(s); nothing ran

Process environment: ${VAR}

${VAR} reads the process environment when the plan loads. It works in two places, env values and connections.*.dsn, and nowhere else in a plan. A step that needs an environment value reads it through env:

env:
  baseUrl: ${VERO_BASE_URL:-http://127.0.0.1:18080}
  tenant: ${TENANT}
connections:
  orders: { driver: postgres, dsn: "${ORDERS_DSN}?application_name=vero-{{ run.id }}" }

An unset variable with no default is a load error: ${TENANT} is not set in the environment. ${VAR:-default} takes the default when VAR is unset or empty, as a shell does:

Written VAR unset VAR set to "" VAR set to x
${VAR} load error "" x
${VAR:-d} d d x
${VAR:-} "" "" x

The default is taken literally and runs to the first }, so it cannot hold a {{ }} template. The variable name follows [A-Za-z_][A-Za-z0-9_]*. ${VAR-d}, ${VAR:=d}, ${VAR:?} and nested expansions are not recognised and stay as written.

Expansion happens before the template parser runs, so a value that comes out of ${VAR} may itself contain {{ run.id }} and be rendered. A value written { secretRef: NAME } takes neither ${VAR} nor templates: it is the whole value of NAME, read at load and scrubbed from every output. ${VAR} values are not secrets and are never scrubbed.

How a value renders

A reference renders as text at the moment the step runs:

  • env.<key> is the value after ${VAR} expansion, with {{ run.id }} rendered once per run. Every task in a run sees the same env.
  • run.id is the run's ULID, the same one the summary line and the event log print.
  • A result renders as its JSON scalar text: a string as itself, without quotes; a number as its exact text, so 10000000000000001 stays 10000000000000001; a boolean as true or false. An object or a list renders as compact JSON on one line.

A result the loader proved declared can still be absent at run time, when its expression evaluated to nil or the upstream never reached the step that sets it. A step whose template needs it ends errored with {{ tasks.login.results.token }}: task login produced no token (its result expression evaluated to nil), and nothing is sent.

A connection's dsn renders on the first use of that connection in the run and is then reused by every step that names it.

Matrix fields

A task with matrix becomes one task per row when the plan loads, named <task>-<key>. Inside that task, {{ matrix.<field> }} is replaced by the row's value as text, before any other check runs, in every string of the task: URLs, headers, bodies, when guards, assertions and results alike. It is substitution, not a reference, so it does not create an edge, and after it nothing in the expanded task remembers the matrix.

  - 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, "body.qty == {{ matrix.qty }}"]

The rules the loader enforces:

  • Every row has a key, and the key follows the task-name grammar: a matrix row needs a key, which names its task create-order-<key>; matrix key "Bulk" must match ^[a-z0-9][a-z0-9-]*$: lower case letters, digits and hyphens; matrix key "bulk" appears twice in task create-order.
  • A row's task name must be free: row bulk would be task create-order-bulk, which the plan already has.
  • Values are literal strings. Quote a number, since qty: 500 is not a string. A {{ in a value is refused: a matrix value is a literal, not a template.
  • Every field the task reads exists in every row: row bulk has no field qty, which the task reads as {{ matrix.qty }}.
  • A matrix has at least one row and at most 100: a matrix has at most 100 rows, this one has 101.

Other tasks name a row, as in {{ tasks.create-order-bulk.results.orderId }}. Naming the base task is a load error that lists the rows. The table is never read from a file or built at run time, because the set of tasks has to be known before the first socket opens. A plan that needs a table from elsewhere needs a script step.

This page is docs/content/reference/templates.md in the repository.

verodocs