# Syntax Reference
## Variables
<!-- markdownlint-disable MD014 -->
```text
$ variable_name = value
$ variable_name = $(command)
$ variable_name = env("NAME")
$ variable_name = secret.env("NAME")
$ variable_name = secret.file("./path/to/value.txt")
$ variable_name = [[ prompt_name ]]
$ variable_name = [[ prompt_name = default_value ]]
$ variable_name = [foo, bar]
```
<!-- markdownlint-enable MD014 -->
- Array assignments expand the request once per value. When a request references multiple array variables, Hen runs the Cartesian product of those values (up to two variables and 128 total combinations).
- Arrays must contain simple scalar values (no whitespace or nested arrays). Their values can still come from commands or prompts.
- Prompt placeholders may include defaults with `[[ name = default ]]`. Defaults are plain text up to the closing `]]`, so URL-shaped values such as `[[ ws_origin = wss://example.com ]]` are valid.
- Whole-value request fields also accept `$(command)` plus the local value-provider forms below, so request URLs, headers, query params, cookies, and similar scalar request fields can resolve shell output, prompt inputs, and direct env/secret lookups at run time.
- Requests generated from arrays are suffixed with the selected values (for example, `[USER=foo]`). A failing iteration aborts the remaining iterations, and exports from each iteration are suffixed with the same label.
- Mapped requests may depend on the matching expanded iteration of another mapped request when both requests share compatible iteration bindings. Unmapped requests still cannot depend on a mapped request, and ambiguous mapped matches still fail.
## Local Value Providers
<!-- markdownlint-disable MD014 -->
```text
$ API_ORIGIN = env("API_ORIGIN")
$ API_TOKEN = secret.env("HEN_API_TOKEN")
$ CLIENT_ID = secret.file("./secrets/client_id.txt")
```
<!-- markdownlint-enable MD014 -->
- `env("NAME")` reads the named process environment value when the collection is prepared for execution and falls back to loaded dotenv values when the process environment does not define that key. Values loaded this way are not auto-redacted.
- `secret.env("NAME")` reads from the same lookup stack but marks the resolved value sensitive.
- `secret.file("PATH")` reads a UTF-8 text file relative to the collection working directory and strips one trailing line ending.
- Repeated environment-backed and secret-backed references are cached once per run after the first lookup.
- These references are valid in scalar assignments and as whole-value request fields such as request URLs, headers, query parameters, cookies, body inputs, and protocol-specific scalar fields.
- Reference arguments are string literals in this slice. Interpolation such as `env("{{ NAME }}")`, `secret.env("{{ NAME }}")`, or `secret.file("[[ path ]]")` is rejected.
- `hen verify` parses and validates these references without reading environment variables or files.
- Supported secret providers in this slice are `env` and `file`. Remote secret backends are out of scope, and OS keychain is not implemented yet.
## Dotenv Directives
```text
dotenv .env
[ PROFILE == "local" ] dotenv .env.local
env local
dotenv .env.local
```
- `dotenv PATH` is only valid in the collection preamble before the first `---`.
- Dotenv directives may appear at top level or inside named `env` blocks.
- Dotenv directives may use the existing bracketed guard prefix. Guards are evaluated before the file is loaded.
- Dotenv paths are plain string-like values in this slice. Interpolation such as `dotenv {{ path }}` or `dotenv [[ path ]]` is rejected.
- `hen verify` validates dotenv syntax and guards, and reports duplicate-path or missing-file warnings from resolved paths, without reading dotenv contents.
- During runs, top-level dotenv directives load first in source order, then dotenv directives from the selected `env` block load in source order. Later files override earlier ones.
- Missing dotenv files are skipped with a warning instead of failing the run.
- Dotenv directives do not create Hen variables automatically. Collections still bind selected keys explicitly through `env("NAME")` or `secret.env("NAME")`.
## Redaction Rules
```text
redact_header = X-Session-Token
redact_capture = SESSION_ID
redact_body = body.session.accessToken
redact_body = json(body.payload).token
```
- Redaction rules are only valid in the collection preamble before the first `---`.
- Built-in masking already covers `Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie`, and API-key style headers, plus values loaded through `secret.env(...)` and `secret.file(...)`, including `secret.env(...)` values sourced from dotenv files.
- `redact_header = NAME` adds one exact header name to the masked header set for text output, structured reports, transcripts, and retained artifacts.
- `redact_capture = NAME` treats captured or exported values under that name as sensitive in the current request and in downstream dependent requests that reuse the exported value.
- `redact_body = ...` accepts the same body-path syntax used by captures, but it must resolve from the current response body. Valid forms start from `body...` or `json(body...). ...`.
- Redaction rules are additive. They broaden the default safe-output policy and do not disable the built-in masking rules.
- `hen verify` validates the directive syntax and body-path shape without resolving any live values.
## Named Environments
```text
$ API_ORIGIN = https://api.example.com
$ CLIENT_ID = [[ client_id ]]
env local
$ API_ORIGIN = http://localhost:3000
$ CLIENT_ID = hen-local
env staging
$ API_ORIGIN = https://staging.example.com
```
- Environment blocks are only valid in the collection preamble before the first `---`.
- Environment overrides may only target previously declared scalar variables.
- Environment blocks may contain scalar overrides plus `dotenv PATH` directives. Other nested syntax such as arrays or shell substitutions remains rejected.
- Select an environment with `hen run --env <name>` or with the MCP `run_hen` tool's `environment` argument.
- If no environment is selected, Hen uses the collection defaults.
- `hen verify` reports `availableEnvironments`, and structured run output reports `selectedEnvironment`.
Resolution order is:
1. Collection preamble scalar assignments.
2. The selected named environment.
3. Explicit CLI `--input key=value` or MCP `inputs` values for prompt placeholders.
4. Prompt defaults declared with `[[ name = default ]]`.
5. Runtime dependency captures and callback exports for downstream requests.
## OAuth Profiles
```text
oauth api
grant = client_credentials
issuer = https://login.example.com
client_id = secret.env("HEN_CLIENT_ID")
client_secret = secret.env("HEN_CLIENT_SECRET")
scope = read:profile
param audience = https://api.example.com
access_token -> $API_ACCESS_TOKEN
token_type -> $API_TOKEN_TYPE
---
Get profile
auth = api
GET https://api.example.com/me
```
- `oauth <name>` blocks are only valid in the collection preamble before the first `---`.
- `auth = <name>` attaches the named OAuth profile to one request.
- Supported grants in this slice are `client_credentials` and `refresh_token`.
- A profile must define exactly one endpoint source: `issuer = ...` or `token_url = ...`.
- `issuer = ...` performs OIDC discovery at run time and caches the discovered `token_endpoint` for the current run.
- `param name = value` adds an extra form field to the token request.
- `<field> -> $VARIABLE` maps token-response fields into ordinary scalar exports that the current request and downstream dependent requests can reference.
- Hen injects `Authorization: Bearer ...` automatically unless the request already sets `Authorization` explicitly.
- `refresh_token` profiles remember a rotated refresh token returned by the token endpoint and use it for later refreshes in the same run.
- Discovery and token acquisition never run during `hen verify`; verification stays structural.
- Mapped export names participate in the existing built-in sensitive-export masking rules and any configured `redact_capture` rules, while `Authorization` headers are already redacted by default.
## Headers
```text
* header_key = header_value
* header_key = {{ variable_name }}
* header_key = [[ prompt_name ]]
* header_key = $(./get_token.sh)
* header_key = secret.env("HEN_API_TOKEN")
```
Request-level headers override global preamble headers when the header name matches exactly.
Use the same casing when overriding a global header; case-only differences are not a reliable override.
## Query Parameters
```text
? query_key = query_value
? query_key = {{ variable_name }}
? query_key = [[ prompt_name ]]
? query_key = env("REGION")
```
## Cookies
```text
@ cookie_name = cookie_value
@ cookie_name = {{ variable_name }}
@ cookie_name = [[ prompt_name ]]
@ cookie_name = secret.env("SESSION_COOKIE")
```
- Cookie directives are valid in the collection preamble and inside individual requests.
- Request-level cookies override preamble cookies when the cookie name matches exactly.
- Hen serializes structured cookies into one outbound `Cookie` header.
- Structured `@ ...` cookies cannot be mixed with a manual `* Cookie = ...` header on the same request.
- When a request also uses `session = ...`, explicit `@ ...` cookies merge with the session cookie jar by name, and explicit request cookies win on conflicts.
```text
@ region = us-east-1
---
Load dashboard
session = web
GET https://example.com/dashboard
@ session = [[ session_id ]]
```
- Use `@ ...` when you want a structured request cookie instead of manually authoring a raw
`Cookie` header.
- Session-backed HTTP requests can combine explicit `@ ...` cookies with response-set cookies from
the shared cookie jar.
## Multipart Form Data
```text
~ form_key = form_value
~ form_key = {{ variable_name }}
~ form_key = [[ prompt_name ]]
~ form_key = @/path/to/file
```
## Request Body
```text
~~~ [content_type]
body
~~~
body_file = @/path/to/file
```
- Use fenced body blocks for inline text payloads.
- Use `body_file = @/path/to/file` to send raw bytes from disk, similar to `curl --data-binary @file`.
- `body_file` paths resolve relative to the `.hen` file.
- Set `* Content-Type = ...` explicitly when the server expects a specific media type such as `application/octet-stream`.
- `body_file` is only for ordinary HTTP requests. GraphQL requires a `~~~graphql` document block, and MCP, SSE, and WebSocket requests do not accept `body_file`.
- See [examples/binary_request_body.hen](examples/binary_request_body.hen) for a runnable example.
## Request Reliability
```text
timeout = 30s
poll_every = 1s
---
Wait for export
GET https://example.com/exports/{{ JOB_ID }}
timeout = 5s
poll_until = 2m
poll_every = 2s
^ & status == 200
^ & body.state == "completed"
```
- `timeout = ...` may appear in the preamble or on a request. It applies per attempt and defaults to `30s`.
- `poll_until = ...` may appear in the preamble or on a request. It sets the total polling window across attempts and is disabled by default.
- `poll_every = ...` may appear in the preamble or on a request. It sets the fixed retry interval and defaults to `1s` whenever polling is enabled and no interval is provided.
- Request-level reliability directives override only the fields they declare and inherit the remaining preamble defaults.
- Durations currently accept `ms`, `s`, or `m` suffixes such as `250ms`, `2s`, or `1m`.
- When polling is enabled, Hen reruns the same request and reevaluates its ordinary assertions on each attempt.
- Polling retries only after assertion failures or per-attempt request timeouts. Transport failures remain terminal unless a separate retry policy is added later.
- `within = ...` remains separate from request reliability. Use `within` only for SSE `receive` steps and WebSocket `receive` or `exchange` steps.
- When the polling window expires, Hen reports the last assertion mismatch or timeout detail rather than a generic poll exhaustion message.
- See [examples/request_reliability.hen](examples/request_reliability.hen) for a complete collection that combines preamble defaults, a captured job ID, and a polled follow-up request.
## Assertion Labels
```text
# The page loads
^ & status == 200
# User payload is present
[FEATURE_ENABLED == true] ^ & body.user.id == "123"
```
- A `# ...` comment directly above an assertion attaches a short plain-text label to that assertion only.
- Labels appear in successful text CLI output so passing assertions can describe intent instead of repeating the raw expression.
- Labels are literal text and are not interpolated.
- Blank lines or other request items break the association.
- Guard predicates use the same comparison operators as assertions, including `===` for typed JSON operands such as `& body...` or `json(...)`.
- In a guard, `===` returns `true` when the value matches the target and `false` on an ordinary schema mismatch. Invalid targets and untyped left-hand operands still fail as authoring or execution errors.
## Protocol Directives
```text
protocol = graphql
operation = GetUser
variables = {"id":"123"}
protocol = mcp
session = app
call = initialize
tool = search
arguments = {"query":"hedgehog"}
protocol_version = 2025-11-25
client_name = hen
client_version = 0.14.0
capabilities = {}
protocol = sse
session = prices
receive
within = 5s
protocol = ws
session = chat
~~~json
{"type":"hello"}
~~~
```
- If `protocol` is omitted, the request is ordinary HTTP.
- `protocol = graphql` enables GraphQL authoring while still executing over HTTP.
- `protocol = mcp` enables MCP-over-HTTP authoring while still executing over HTTP.
- `protocol = sse` enables server-sent event streams with an opening request plus session-backed `receive` steps.
- `protocol = ws` enables session-backed WebSocket open, `send`, `exchange`, and `receive` steps.
- GraphQL requests currently require `POST`.
- MCP requests currently require `POST`.
- SSE opening requests currently require `GET`.
- WebSocket opening requests currently require `GET`.
- `variables = ...` must be valid JSON after interpolation.
- `call = ...` selects the MCP JSON-RPC method for the request.
- `session = ...` names a reusable session handle. For ordinary HTTP requests, steps sharing the same session reuse one cookie jar. When a later session-backed request omits its method/URL line, Hen inherits the most recent method/URL used for that session.
- For `protocol = ws`, a body block implies a send step. Plain `~~~` or `~~~text`/`~~~text/plain` sends text, while `~~~json` or `~~~application/json` sends JSON.
- `receive` is valid for `protocol = sse` and `protocol = ws` steps.
- For `protocol = ws`, adding `within = ...` to a body-backed step turns it into an exchange step.
- `within = ...` is valid for `protocol = sse` receive steps, `protocol = ws` receive steps, and `protocol = ws` exchange steps, and currently accepts `ms`, `s`, or `m` suffixes such as `250ms`, `2s`, or `1m`. Use request-level `timeout`, `poll_until`, and `poll_every` for ordinary execution timeouts and polling.
- `tool = ...` and `arguments = ...` are only valid with `call = tools/call`.
- `protocol_version`, `client_name`, `client_version`, and `capabilities` are only valid with `call = initialize`.
- `arguments = ...` and `capabilities = ...` must be valid JSON objects after interpolation.
## HTTP Session Cookies
```text
Login
session = web
POST https://httpbin.org/cookies/set/session/hen-demo
# Login request succeeds
^ & status == 200
---
Load cookies
session = web
GET https://httpbin.org/cookies
# Reuses the session cookie
^ & body.cookies.session == "hen-demo"
```
- Reusing the same HTTP `session = ...` shares one cookie jar across those steps.
- Session-backed HTTP requests also get the same implicit planner ordering Hen already uses for MCP, SSE, and WebSocket session reuse.
- Explicit `@ cookie = value` lines are layered on top of the session cookie jar for that request. Non-conflicting session cookies are preserved, while explicit cookies override same-name jar cookies.
- Cookie values are covered by the built-in redaction rules for `Cookie` and `Set-Cookie`.
## GraphQL Requests
```text
Get User GraphQL
protocol = graphql
POST https://example.com/graphql
operation = GetUser
variables = {"id":"123"}
~~~graphql
query GetUser($id: ID!) {
user(id: $id) {
id
}
}
~~~
^ & graphql.errors == null
^ & graphql.data.user.id == "123"
```
- Use `~~~graphql` or `~~~application/graphql` for the document block.
- GraphQL responses still use the normal `body`, `header`, and `status` accessors.
- `graphql.data...` and `graphql.errors...` are aliases for the corresponding JSON body paths.
- Callback environments receive `GRAPHQL_DOCUMENT`, `GRAPHQL_OPERATION`, and `GRAPHQL_VARIABLES` for GraphQL requests.
## MCP Requests
```text
Connect MCP
protocol = mcp
session = app
POST https://example.com/mcp
* Authorization = Bearer [[ mcp_token ]]
call = initialize
^ & body.result.serverInfo.name == "fixture-mcp"
---
List resources
protocol = mcp
session = app
call = resources/list
^ & body.result.resources[0].name == "Fixture Resource"
---
Call tool
protocol = mcp
session = app
call = tools/call
tool = search
arguments = {"query":"hedgehog"}
^ & body.result.content[0].text == "search result"
```
- Supported `call = ...` values in the current slice are `initialize`, `tools/list`, `resources/list`, and `tools/call`.
- `call = initialize` defaults `protocol_version` to Hen's preferred MCP version, `client_name` to `hen`, `client_version` to the running Hen build version, and `capabilities` to `{}` unless you override them.
- MCP request bodies are generated automatically as JSON-RPC envelopes, so explicit body blocks, content-type blocks, and multipart form directives are not allowed.
- MCP authentication currently uses ordinary request headers.
- MCP responses still use the normal `body`, `header`, and `status` accessors, so assertions and captures can target `body.result...` and `body.error...` directly.
- When an MCP tool returns stringified JSON inside a text field, use `json(...)` to decode it before traversing or asserting on nested fields, for example `^ & json(body.result.content[0].text).items[0].id == "123"`.
- Session-backed steps add an implicit dependency on the session-creating request. When a later step omits its method/URL line, Hen inherits the most recent method/URL used for that session.
## SSE Requests
```text
Open price stream
protocol = sse
session = prices
GET https://example.com/prices/stream
^ & status == 200
---
Receive update
session = prices
receive
within = 5s
& sse.id -> $EVENT_ID
^ & sse.event == "price"
^ $EVENT_ID == "evt-1"
^ & body.symbol == "AAPL"
^ & body.price === NUMBER
```
- The opening SSE step establishes the named session and leaves the stream reader running in the background.
- If the opening step omits an explicit `Accept` header, Hen sends `Accept: text/event-stream` automatically.
- `receive` consumes the next event observed for the session within the configured timeout.
- When the event payload is valid JSON, ordinary `body...` JSON paths work for assertions and captures.
- Receive steps can also capture or assert `sse.event` and `sse.id` directly when the server sends them.
- When the payload is plain text, use the normal raw-body comparison or regex assertions.
- Callback environments include `SSE_ACTION`, `SSE_SESSION`, and, for receive steps that observed them, `SSE_EVENT` and `SSE_ID`.
- Structured JSON output preserves SSE protocol context such as `action`, `sessionName`, `within`, `event`, and `id` when available.
## WebSocket Requests
```text
Open socket
protocol = ws
session = chat
GET wss://example.com/chat
---
Send hello
session = chat
within = 2s
~~~json
{"type":"hello"}
~~~
& ws.kind -> $KIND
^ $KIND == "text"
^ & ws.kind == "text"
^ & body.type == "ack"
```
- The opening WebSocket step establishes the named session.
- A body block makes a WebSocket step a send step. Plain `~~~` or `~~~text`/`~~~text/plain` sends text, while `~~~json` or `~~~application/json` sends JSON.
- Adding `within = ...` to that body-backed step turns it into an exchange step that waits for the next queued message.
- Later session-backed `send`, `exchange`, and `receive` steps inherit the target line from the session-opening step.
- `receive` and `exchange` reuse the shared `within = ...` timeout grammar.
- `ws.kind` can capture or assert the observed reply frame type directly, while `body` contains the reply payload.
- Callback environments include `WS_ACTION`, `WS_SESSION`, and, for send, exchange, and receive steps, `WS_KIND`.
- Structured JSON output preserves WebSocket protocol context such as `action`, `sessionName`, `within`, and `kind` when available.
To run the repository WebSocket example end to end, use `hen ./examples/ws_protocol.hen all` because the file contains both the session-opening step and the exchange step.
## Declarations
Declarations live in the collection preamble before the first `---`. They are collection-local, and imported fragments brought in with `<<` can contribute reusable declarations.
## Fragments
```text
<< ./fragments/common_schema_types.hen
[ FEATURE_ENABLED == true ] << ./fragments/extra_assertions.hen
```
- `<< path.hen` imports another Hen file into the current collection before parsing.
- Fragment paths are resolved relative to the importing file.
- Imports can contribute reusable declarations or request-local snippets.
- Prefix a fragment import with `[predicate]` to include it only when the guard evaluates to true.
- Guarded fragment imports use the same guard rules described below.
```text
scalar Food = enum("pizza", "taco", "salad")
scalar CardKind = const("card")
scalar HANDLE = string & len(3..24) & pattern(/^[a-z][a-z0-9_]*$/)
schema Address {
city: string
postalCode: string
}
schema User {
id: UUID
email: EMAIL
birthday?: DATE?
favoriteFood?: Food
address: Address
}
schema ContactAddress = EMAIL
schema Users = User[]
schema CardCheckout {
method: CardKind
cardLast4: string
}
schema BankCheckout {
method: string
accountId: string
}
schema PaymentMethod = oneOf(CardCheckout, BankCheckout)
schema Contact = anyOf(EMAIL, URI)
schema CombinedCheckout = allOf(Checkout, BankCheckout)
schema NonCardCheckout = not(CardCheckout)
schema Checkout = discriminator(method,
"card": CardCheckout,
"bank": BankCheckout
)
```
- Built-in scalar targets are always available: `UUID`, `EMAIL`, `NUMBER`, `DATE`, `DATE_TIME`, `TIME`, and `URI`. Those names are reserved and cannot be redefined.
- `scalar` declarations can reference primitive types, built-in scalar targets, or named scalar declarations, then refine them with:
`const(value)` for an exact literal match; `enum(value1, value2, ...)` where each value is a quoted string, integer, decimal number, `true`, `false`, or `null`; `format(NAME)` where `NAME` is one of `UUID`, `EMAIL`, `NUMBER`, `DATE`, `DATE_TIME`, `TIME`, or `URI`; `len(min..max)` for string length checks with inclusive integer bounds and optional open-ended forms like `len(3..)` or `len(..24)`; `pattern(/regex/)` for slash-delimited regular-expression checks on strings; and `range(min..max)` for inclusive integer or decimal numeric bounds with optional open-ended forms like `range(0..)` or `range(..10.5)`.
- Scalar and schema references are resolved after the full preprocessed collection is loaded, so forward references are allowed across the collection preamble and imported fragments.
- `schema` object fields are open by default in v1: extra fields are ignored during validation.
- Schema field names may be bare identifiers, `$`-prefixed names such as `$metadata`, or quoted strings such as `"request-id"` when the JSON key is not a plain identifier.
- Schema object fields may optionally end with a trailing comma, so JSON-style layouts like `field: Type,` are valid.
- `schema Name = Target` aliases another target.
- `field?: Type` makes a field optional.
- `Type?` makes the value nullable.
- `schema Name = Type[]` defines a root-array schema.
- `schema Name = allOf(A, B, ...)` requires all listed targets to validate.
- `schema Name = oneOf(A, B, ...)` requires exactly one listed target to validate.
- `schema Name = anyOf(A, B, ...)` requires at least one listed target to validate.
- `schema Name = not(Target)` requires the target not to validate.
- `schema Name = discriminator(field, "tag": Target, ...)` selects a branch by one field value,
then validates the matching branch target.
- `allOf(...)`, `oneOf(...)`, and `anyOf(...)` require at least two targets. `discriminator(...)`
requires at least one branch.
## JSON Selection
Use JSON selectors anywhere Hen expects a response-body operand, including captures, assertions, redaction rules, and dependency reads.
```text
& body.user.id -> $USER_ID
^ & body.items[0].name == "first"
^ & body.[0].id == 123
^ & body.jobs[? (recipient == $RECIPIENT || recipient == "fallback@example.com") && status != "failed"].status == "succeeded"
^ & json(body.result.content[0].text).items[0].id == "123"
^ &[Create Job].body.result.state == "completed"
```
- `body` starts at the current response JSON value.
- Use dot access for object fields such as `body.user.id`.
- Use `[index]` for arrays nested under an object key, such as `body.items[0].name`.
- Use `body.[index]` when the response body itself is an array.
- Use `[? ...]` to query an array for one matching element, then continue traversing from that element.
- Filter queries evaluate paths relative to each array item, support `==` and `!=`, accept scalar literals or `$VARIABLE` values, and may combine clauses with `&&`, `||`, and parentheses.
- Filter queries must resolve to exactly one array element. Zero matches or multiple matches are treated as failures.
- Use `json(...)` when the selected value is a JSON string and you want to decode it before continuing traversal.
- Use `&[Request Name].body...` when selecting from a declared dependency response instead of the current response.
- Hen intentionally keeps selectors narrower than full JSONPath or jq: no wildcards, slices, projections, recursive descent, nested filters, regex filters, or structural filter RHS values.
## Response Captures
```text
& body -> $VAR_NAME
&[Dependency Request].body -> $VAR_NAME
& body.json_field -> $VAR_NAME
& body.token -> $TOKEN := fallback
& body.items[0].name -> $VAR_NAME
& body.[0].id -> $VAR_NAME
& body.jobs[? recipient == "alice@example.com"].status -> $VAR_NAME
& json(body.result.content[0].text).items[0].id -> $VAR_NAME
& header.header_name -> $VAR_NAME
& status -> $VAR_NAME
```
Use the JSON selection rules above for `body...`, `json(...)`, and dependency-body paths.
Use `&[Request Name].body...`, `&[Request Name].header...`, or `&[Request Name].status` to read from a declared dependency response.
Dependency response captures require the referenced request to be declared in `> requires:` so the planner can guarantee the upstream result exists.
Append `:= default_value` to use a fallback when the selected capture path is missing.
## Assertions
`val` can be a variable or a response capture.
```text
^ & body.field == 'value'
^ & body.jobs[? recipient == "alice@example.com"].status == 'succeeded'
^ & sse.event == 'price'
^ & json(body.result.content[0].text).items[0].id == '123'
^ & json(body.result.content[0].text).items[0] ~= {"id":"123"}
^ & body.id === UUID
^ & body.total === NUMBER
^ & body === User
^ &[Login].body === LoginResponse
& body.field -> $VAR
^ $VAR == 'value'
```
```text
^ val == value
^ val != value
^ val ~= /regex/
^ val ~= {"id":7}
^ val ~= [{"id":1},{"id":2}]
^ val === SchemaName
^ val > value
^ val >= value
^ val < value
^ val <= value
```
- Bare `true`, `false`, and `null` are typed literals.
- Quoted values remain strings.
- Prefix an assertion with `[predicate]` to evaluate it conditionally.
- Response body assertions preserve JSON types during evaluation, including arrays, objects, `null`, and missing paths.
- `==` and `!=` compare structured JSON values structurally when both sides resolve to arrays or objects.
- `~=` performs substring or regex matching for string patterns, partial object matching for JSON object literals, unordered membership for array-vs-scalar JSON matches, and unordered subset matching for JSON array literals.
- `===` validates the left-hand JSON value against a built-in scalar, named scalar, or named schema target from the collection preamble.
- The right-hand side of `===` must be a declared target name, not a variable or quoted literal.
- `===` requires a typed JSON left-hand operand, so use response body captures or dependency body captures rather than plain string variables.
- `json(...)` decodes stringified JSON from a response path before the usual JSON traversal and assertion rules apply.
- Use `NUMBER` when you want a concise built-in target for any JSON number. For text validation, the current recommended path is a named scalar built from `string` with `len(...)`, `pattern(...)`, `enum(...)`, or `format(...)` as needed.
- Body capture operands use the same JSON selection rules as response captures, including filtered array queries with `$VARIABLE` values and the same exact-one-match requirement.
- JSON object and array literals must be valid JSON with double-quoted keys and string values.
- Ordering operators only succeed for comparable numeric or text values.
## Conditional Guards
```text
[ USERNAME == "foo" ] ^ & body.username == "foo"
[ FEATURE_ENABLED != true ] << ./fragments/legacy_assertions.hen
```
- Guards apply to assertions and fragment imports.
- The guard predicate must appear in `[...]` immediately before the guarded assertion or `<<` import.
- Guard predicates use the same comparison operators and regex matching as ordinary assertions.
- Guard predicates may reference interpolated variables and typed literals such as `true`, `false`, and `null`.
- `===` schema validation is not supported inside guards.
- A false assertion guard marks that assertion as skipped.
- A false fragment guard omits that fragment import entirely.
## Callbacks
```text
! callback code
! ./path/to/callback_file.sh
! some command -> $VARIABLE_NAME
```
Assignments require whitespace around `->` and suppress the command's stdout.
## Dependencies
```text
> requires: Request Name
```
- Dependencies create a DAG so prerequisite requests run before their dependents.
- Dependency captures still require the referenced request to be declared in `> requires:`.
- When a dependency name expands into multiple mapped requests, a mapped dependent resolves to the single expanded request whose iteration bindings are compatible with its own iteration.
- Unmapped dependents, zero compatible matches, and multiple compatible matches remain errors.