Reference values between blocks
The {{...}} syntax is what turns httui from “a markdown editor”
into “a chained runtime”. This guide is the complete cheatsheet for
references: where they come from, where they can go, what scoping
rules apply.
Anatomy of a reference
Section titled “Anatomy of a reference”{{ alias . field . path }} ^^^^^ ^^^^^ ^^^^ | | | | | JSON path into the field | body | status | headers | cookies | row the alias of the block whose response you wantWhitespace inside {{ ... }} is allowed but discouraged for grep-ability.
Fields available on every block response
Section titled “Fields available on every block response”| Field | What it is | Example |
|---|---|---|
body | JSON-parsed response body (or text if not JSON) | {{login.body.token}} |
status | numeric HTTP status code | {{login.status}} |
headers.<name> | response header value | {{login.headers.x-request-id}} |
cookies.<name> | set-cookie value | {{login.cookies.session}} |
size | response body length in bytes | {{download.size}} |
time | total elapsed in ms | {{slow.time}} |
DB blocks have one extra field:
| Field | What it is | Example |
|---|---|---|
row[N] | nth row of the result set | {{users_list.row[0].email}} |
rows | full array of rows | {{users_list.rows}} (rare) |
affected | rows affected by INSERT/UPDATE/DELETE | {{seed.affected}} |
JSON paths
Section titled “JSON paths”After body. you can drill into any JSON shape with dot notation
and [N] indexing:
```http alias=orderGET {{BASE_URL}}/orders/{{previous.body.data.items[0].id}}Authorization: Bearer {{login.body.tokens.access_token}}```| Pattern | Reads |
|---|---|
body.user.id | { user: { id: 42 } } → 42 |
body.items[0].name | { items: [{ name: "x" }] } → "x" |
body.tags[2] | { tags: ["a", "b", "c"] } → "c" |
body.nested["odd key"] | bracket-quoted for non-identifier keys |
body[*].name | NOT supported — use chained blocks or expects |
Positional shortcut: {{$prev}}
Section titled “Positional shortcut: {{$prev}}”When you don’t care about naming, $prev is “the previous executed
block, response as the implicit root”:
```http alias=loginPOST {{BASE_URL}}/auth/login```
```httpGET {{BASE_URL}}/users/{{$prev.body.user.id}}```{{$prev.body.user.id}} is exactly {{login.body.user.id}}. Use
when the alias is obvious from context (one-shot script style).
Where references can go
Section titled “Where references can go”References resolve at these positions before the block dispatches:
| Block type | Position | Example |
|---|---|---|
| HTTP | URL | GET {{BASE_URL}}/x |
| HTTP | header key | {{HEADER_NAME}}: value |
| HTTP | header value | Authorization: Bearer {{token}} |
| HTTP | body (any content type) | { "id": {{user.body.id}} } |
| HTTP | query string | ?since={{cutoff.body.iso}} |
| DB | SQL text | SELECT * FROM x WHERE id = {{user.body.id}} |
| DB | connection params (host/port via session override) | {{PG_HOST}} |
| Standalone | block body | depends on block type |
References do not resolve in info-string tokens
(alias=..., timeout=...) — those are configuration, not data.
SQL references are always bound
Section titled “SQL references are always bound”When a {{...}} shows up in a SQL block, httui does not
string-interpolate it. The reference becomes a bind parameter
($1 for Postgres, ? for SQLite/MySQL), and the resolved value
goes through the driver’s prepared-statement binding:
```db-localSELECT * FROM ordersWHERE customer_id = {{user.body.id}} AND created_at > {{cutoff.body.iso}}```What the driver sees:
SELECT * FROM ordersWHERE customer_id = $1 AND created_at > $2Bind values: [user.body.id, cutoff.body.iso].
Zero SQL injection surface, even with user-controlled values flowing in through HTTP responses.
Priority: block > env var
Section titled “Priority: block > env var”If you have a block aliased BASE_URL and an env var also called
BASE_URL, the block wins when both are in scope:
```http alias=BASE_URLGET https://dynamic-discovery.example.com/url```
```httpGET {{BASE_URL}}/health # uses the block's response body as the URL!```This is by design (gives runbooks a way to override env values at runtime), but easy to surprise yourself. Rule of thumb: use ALL_CAPS for env vars, snake_case or kebab-case for block aliases.
Auto-execution: the DAG
Section titled “Auto-execution: the DAG”When you run a block that references {{other.body.X}}, httui:
- Parses the references in your block.
- Finds
otheris an alias defined above the current block. - Checks the cache — if
other’s inputs (body, env, refs) hash matches the cached run, uses the cached response. - Otherwise runs
other(which may itself trigger upstream dependencies — recurse). - Substitutes the resolved value into your block.
- Runs your block.
References can only point upward in the file. This makes the runbook a DAG by construction — no cycles possible, no “block 3 references block 7 which references block 2” loops.
When the value is missing
Section titled “When the value is missing”| Cause | What you see |
|---|---|
| Alias doesn’t exist in any block above | Editor underlines the ref red; hover shows “unknown alias” |
| Field path doesn’t exist in the response | Block runs, panel shows error: “path body.user.id not found in response” |
| Upstream block hasn’t run yet | httui runs it first (auto-exec) |
| Upstream block errored | The error propagates — downstream block doesn’t run, panel shows “upstream login failed” |
Inspecting resolved values
Section titled “Inspecting resolved values”| Where | How |
|---|---|
| In editor | Hover the {{...}} — popup shows resolved value or error |
| Before run | The popup updates live as the cache fills |
| After run | Raw tab of the response panel shows the literal request, all refs already substituted |
| In drawer | Click ⚙ on the block toolbar → References tab lists every ref with current value |
Related
Section titled “Related”- Build a chained API test — see references in action with a full login → me → assert flow.
- Use environment variables —
{{KEY}}without dots (no.body, no.status— just a flat string lookup). - Assert response shape — using refs
inside
# expect:to compare across blocks.