Skip to content

Captures & result chaining

capture blocks copy values out of a step’s response (or request) into named slots under result.<step>.<key>. Later steps in the same scenario can read those slots in any expression: URL templates, request bodies, headers, assertions, even other captures.

step "http" "create_user" {
request {
method = "POST"
url = "${config.base_url}/users"
body {
json = {
password = "Sup3rS3cret!"
}
}
}
expect {
status = 201
json = {
id = is_string()
email = request.body.json.email
}
}
capture {
id = response.json.id
email = response.json.email
}
}

After this step:

ExpressionValue
result.create_user.idthe JSON id field
result.create_user.emailthe JSON email field
step "http" "get_user" {
request {
method = "GET"
url = "${config.base_url}/users/${result.create_user.id}"
}
expect {
status = 200
json = {
email = result.create_user.email
}
}
}

You can mix capture references with literal values, env vars, generators, etc., anywhere an expression is valid.

capture blocks accept any HCL expression. Common patterns:

capture {
# Direct JSON fields
id = response.json.id
email = response.json.email
# Nested paths
first_tag = response.json.tags[0]
# Header values
request_id = response.headers["X-Request-Id"]
# Status code (rarely useful but possible)
status = response.status_code
# Echoes of the request, useful for tests that round-trip
sent_email = request.body.json.email
# Computed values
greeting = "Hello, ${response.json.first_name}!"
}

For SQL steps, response.json.rows[i].<column> works the same way, see SQL provider for the row shape.

For mobile steps, the runtime injects two extra helpers, value("id") and text("id"), which read the current accessibility tree:

capture {
current_text = text("login.email")
current_value = value("login.email")
}

A step may only read captures from steps defined earlier in the file. The parser checks this at load time and exits with code 2 on a forward or unknown reference.

step "http" "first" {
// ...
expect {
json = {
id = result.second.id // ERROR: forward reference to a later step
}
}
}
step "http" "second" {
// ...
capture {
id = response.json.id
}
}

The same applies to depends_on. Source order is the source of truth.

Captures never cross scenario boundaries. Each scenario starts with an empty result namespace. To share state between scenarios, persist it externally (database row, file, env var injected at the suite level), or, more often, refactor the dependency into a keyword.

If a step is skipped (via skip_if, skip_unless, or because a dependency failed/skipped), its capture block does not run. Any downstream step referencing result.<skipped_step>.<key> is automatically skipped with a depends on skipped step "..." reason, see Conditional execution.

This means you can write naïve downstream steps without defensive can(...) checks, the cascade handles it for you.