# Syntax Reference
## Variables
```text
$ variable_name = value
$ variable_name = $(command)
$ variable_name = [[ prompt_name ]]
$ variable_name = [[ prompt_name = default_value ]]
$ variable_name = [foo, bar]
```
- 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.
- 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.
- Requests cannot declare dependencies on a mapped request; share setup through an unmapped helper instead.
## Headers
```text
* header_key = header_value
* header_key = {{ variable_name }}
* header_key = [[ prompt_name ]]
```
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 ]]
```
## 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
~~~
```
## 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. 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`.
- `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.
## 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.
```text
scalar Food = enum("pizza", "taco", "salad")
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 Users = User[]
```
- 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 `enum`, `format`, `len`, `pattern`, and `range` predicates.
- 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.
- `field?: Type` makes a field optional.
- `Type?` makes the value nullable.
- `schema Name = Type[]` defines a root-array schema.
## Response Captures
```text
& body -> $VAR_NAME
&[Dependency Request].body -> $VAR_NAME
& body.json_field -> $VAR_NAME
& 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 `body.field` for object-root JSON responses.
Use `body.items[0].name` for arrays nested under object keys.
Use `body.[0].field` when the response body root is an array.
Use `json(...)` when a field contains JSON encoded as a string and you want to traverse the decoded structure.
Use `&[Request Name].body...`, `&[Request Name].header...`, or `&[Request Name].status` to read from a declared dependency response.
Capture paths support structured field access, numeric indices, and simple array filters of the form `[? path == value]` or `[? path != value]`.
Filter clauses may be combined with `&&`, operate on relative paths within the current array item, and must resolve to exactly one element.
Filters are intentionally narrow in v1: scalar literals or `$VARIABLE` references only, no regex or structural JSON matching inside filters, no `||`, and no jq or JSONPath projections.
Dependency response captures require the referenced request to be declared in `> requires:` so the planner can guarantee the upstream result exists.
## 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.
- 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 can use the same filtered array selector syntax as response captures, including `$VARIABLE` filter values, with 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.
## 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
```