Skip to content

Step-local vars

A vars { ... } block declares step-local variables that are evaluated once, in source order, before the provider runs. They exist for the lifetime of the step and are not propagated to other steps.

vars is the right tool for:

  • Timestamps and signing material that must stay stable across request / expect / capture (now_unix() would otherwise be re-evaluated on each call).
  • JSON payloads built once and then hashed for HMAC signing.
  • Generated values you want to reference twice in the same step (a password and its confirmation field, for example).
step "http" "signed_call" {
vars {
ts = now_unix()
body = jsonencode({ id = "evt-1", type = "ping" })
sig = hmac_sha256_hex(config.webhook_secret, "${vars.ts}.${vars.body}")
}
request {
method = "POST"
url = "${config.base_url}/webhook"
headers = { X-Signature = "t=${vars.ts},v1=${vars.sig}" }
body { raw = vars.body }
}
expect {
status = 200
}
}
  • Source order is preserved. Later vars can reference earlier ones via vars.<name>.
  • One evaluation per step. vars are computed once, before the provider runs. Subsequent request / expect / capture see the same value.
  • Step-scoped. A var declared in step A is invisible to step B. To share values across steps, use capture.
  • Visible to request, expect, capture of the same step. They are not visible to when, skip_if, or skip_unless, those are evaluated before the step body runs.

You could inline now_unix() directly in request.headers. But:

  1. now_unix() reads the wall clock at each evaluation. The header value and the body signed with that header would not match.
  2. Computing jsonencode(payload) and hmac_sha256_hex(secret, payload) separately would compute jsonencode twice, once for the body, once nested inside the HMAC, risking the two values diverging if the payload contained any non-determinism.

vars makes the “compute once, use many times” pattern explicit and unambiguous.

Tales validates the body of when, skip_if, and skip_unless at load time. Any vars.<name> reference there produces a parse error (exit code 2) pointing at the offending block.

step "http" "bad" {
vars { id = "evt-1" }
when = can(vars.id) // ERROR: vars are not visible in `when`
// ...
}

The fix: capture the value upstream or compute it inside an expression that runs before the step body (config, result.<earlier_step>, host, env(...), or an expression already free of vars).

A common pattern: compute base values first, derive composite values from them:

vars {
ts = now_unix()
body = jsonencode({
id = "evt-${result.upstream.id}"
type = "notarization.completed"
})
signature = hmac_sha256_hex(config.webhook_secret, "${vars.ts}.${vars.body}")
header = "t=${vars.ts},v1=${vars.signature}"
}
request {
headers = { X-Signature = vars.header }
body { raw = vars.body }
}

The full webhook signing recipe is in the Signing webhooks guide.