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 }}whenenvhas noapiKey:{{ 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 calledlogn: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 }}whenlogindeclares notokn: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 declaresx: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.resetstep reading any task:a reset step runs before any task, so it cannot read {{ tasks.login.results.token }}. - A main task referencing a
finallytask: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 sameenv.run.idis 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
10000000000000001stays10000000000000001; a boolean astrueorfalse. 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: 500is 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.