hen 0.16.0

Run protocol-aware API request collections from the command line or through MCP.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# Syntax Reference

## Variables
<!-- markdownlint-disable MD014 -->
```text
$ variable_name = value
$ variable_name = $(command)
$ 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.
- 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.

## Local Secret Providers

```text
$ API_TOKEN = secret.env("HEN_API_TOKEN")
$ CLIENT_ID = secret.file("./secrets/client_id.txt")
```

- `secret.env("NAME")` reads the named environment variable when the collection is prepared for execution.
- `secret.file("PATH")` reads a UTF-8 text file relative to the collection working directory and strips one trailing line ending.
- Repeated secret references are cached once per run after the first lookup.
- Secret references are valid anywhere this slice accepts scalar assignments, including collection variables, request variables, and environment overrides.
- Secret reference arguments are string literals in this slice. Interpolation such as `secret.env("{{ NAME }}")` or `secret.file("[[ path ]]")` is rejected.
- `hen verify` parses and validates secret references without reading environment variables or files.
- Supported providers in this slice are `env` and `file`. Remote secret backends are out of scope, and OS keychain is not implemented yet.

## 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(...)`.
- `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 values in this milestone must stay scalar; arrays, shell substitutions, and nested environment syntax are 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.

## 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
```