hen 0.9.0

Run API collections from the command line.
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
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
# Hen

Run API requests as files, from the command line.

```text
name = Test Collection File
description = A collection of mock requests for testing this syntax.

$ api_key = $(./get_secret.sh)
$ username = $(echo $USER)
$ api_origin = https://lorem-api.com/api

---

# Load other requests.
<< .fragment.hen

---

Some descriptive title for the prompt.

POST {{ api_origin }}/echo/[[ foo ]]

* Authorization = {{ api_key }}

? query_param_1 = value
? username = {{ username }}

~~~ application/json
{ "lorem" : "ipsum" }
~~~

! sh ./callback.sh
```

## Installation

```bash
cargo install hen
```

## Usage

```text
Usage: hen [OPTIONS] [PATH] [SELECTOR]

Arguments:
  [PATH]
  [SELECTOR]

Options:
  --export
  --benchmark <BENCHMARK>
  --input <KEY=VALUE>
  --parallel
  --max-concurrency <MAX_CONCURRENCY>
  --continue-on-error
  -v, --verbose
  -h, --help                   Print help
  -V, --version                Print version
```

### Execute a Request

To execute a request, use the `hen` command on a file containing a request:

```bash
hen /path/to/collection_directory/collection_file.hen
```

This will prompt you to select a request from the file to execute.

### Specifying a Request

You can specify the nth request directly by providing an index as the second argument:

```bash
hen /path/to/collection_directory/collection_file.hen 0
```

This will bypass the request selection prompt and execute the first request in the file.

Conversely, all requests can be executed with the `all` selector:

```bash
hen /path/to/collection_directory/collection_file.hen all
```

### Selecting a Collection

Alternatively, you can specify a directory of collections.  This will prompt you to select a collection file and then a request from that file.

```bash
hen /path/to/collection_directory
```

> If the directory contains only one collection file, that file will be selected automatically, bypassing the prompt.
> Dotfiles (files starting with `.`) are ignored by the prompt.

### Parallel Execution

Hen executes requests sequentially by default. Pass `--parallel` to run independent requests concurrently according to the dependency graph built from `> requires:` declarations.

```bash
hen --parallel ./collection.hen all
```

- `--max-concurrency <N>` throttles the number of simultaneous requests. Omit or set to `0` to allow as many parallel requests as the graph permits.
- `--continue-on-error` keeps running any branches that are unaffected by a failure. Without this flag Hen stops after the first failed request.
- Outputs are still printed in plan order. Each request buffers its stdout until completion to keep logs deterministic.

## Defining an API Request

An API request is defined in a text file with the `.hen` extension.

At a minimum, a request must have a method and a URL. The method is one of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, or `OPTIONS`. The URL is the endpoint of the request.

```text

[description]

METHOD url

[* header_key = header_value]

[? query_key = query_value]

[~ form_key = form_value]

[~~~ [content_type]
body
~~~]

[> requires: Request Name]

[& response_capture]

[ [^ assertion_condition] || "failure message"]

[! callback]

```

### Headers

Headers are key-value pairs that are sent with the request. Headers are specified with the `*` character.  For example,

```text
* Authorization = abc123
* Content-Type = application/json
```

### Query Parameters

Query parameters are key-value pairs that are appended to the URL. Query parameters are specified with the `?` character. For example,

```text
? page = 2
? limit = 10
```

### Multipart Form Data

Multipart form data is used to send files or text fields with a request. Multipart form data is specified with the `~` character. For example,

```text
$ file_1 = $(cat ./test_file.txt)
---

POST https://lorem-api.com/api/echo

# form data can be used to send text data
~ form_text_1 = lorem ipsum.
~ form_text_2 = {{ file_1 }}

# form data can also be used to send files
~ file_1 = @./test_file.txt
```

### Request Body

The request body is the data sent with the request. The body is a multiline block specified with the `~~~` characters. The body can optionally be followed by a content type. For example,

```text
~~~ application/json
{
  "key": "value"
}
~~~
```

### User Prompts in Requests

User input can be requested interactively at runtime by using the `[[ variable_name ]]` syntax.  A prompt may be used as a value for a query, header, form, or in the request body or URL. For example,

```text
GET https://example.com/todos/[[ todo_id ]]

? page = [[ page ]]
* Origin = [[ origin ]]
~ file = @[[ file_path ]]
```

Prompts made in a request will be displayed in the terminal when the request is executed.

#### Supplying Prompt Values from the CLI

Use the `--input` flag to pre-fill prompt values and skip interactivity. Each `--input` expects `key=value` and may be repeated. Keys correspond to the placeholder name inside `[[ ... ]]`.

```bash
hen --input foo=bar --input password=secret ./collection.hen
```

When a matching prompt is encountered, the provided value is used automatically. Any remaining prompts still fall back to the interactive dialog.

### Response Captures

Response captures extract data from the response and store it in a variable for use in subsequent requests, callbacks, and assertions.  The response is denoted with the `&` character, and can exposes the status code, headers, and body of the response.

```text
& status > $STATUS
& header.content-type > $CONTENT_TYPE
& body > $BODY
```

For JSON bodies, you can extract specific fields.

```text
& body.token > $TOKEN
& body.data.user.id > $USER_ID
& body.items[0].name > $FIRST_ITEM_NAME
```

Response captures update the callback execution context, making the captured variables available to any callbacks defined later in the same request, or in subsequent requests in the same collection.

```text
Get JWT

POST {{ API_URL }}/jwt

~~~ application/json
{
  "username": "foo",
  "password": "[[ password ]]"
}
~~~

& body.token > $TOKEN

! echo $TOKEN | cut -d '.' -f2 | base64 --decode | jq .
```

#### Referencing Dependency Responses

When a request declares dependencies with `> requires:`, it can also read from those responses using the capture syntax. Prefix the capture path with `&[Request Name]` to target the upstream response:

```text
> requires: Get JWT

GET https://api.example.com/profile

&[Get JWT].body.token > $TOKEN
&[Get JWT].status.code > $LOGIN_STATUS
```

The same accessors (`body`, `header`, `status`) are supported. If a capture references a request that has not been declared as a dependency, Hen raises an error during parsing.

### Assertions

Assertions provide lightweight response validation without leaving the request file. Each assertion line starts with `^` followed by a boolean expression. Assertions run after captures resolve and before callbacks execute. If any assertion fails, request execution stops and the failure message is surfaced.

```text
POST https://api.example.com/login

& body.token > $TOKEN
^ $TOKEN ~= /[A-Za-z0-9_-]+/ || "Token missing or malformed"
^ STATUS == 200
```

Available operators are `==`, `!=`, `<`, `<=`, `>`, `>=`, and `~=`. The `~=` operator acts as a substring match for quoted strings and as a regular expression when the right-hand side is wrapped in `/` delimiters.

Assertions can reference the following values:

- Response metadata: `STATUS`, `STATUS_TEXT`, `RESPONSE`, and `DESCRIPTION`.
- Any response captures defined earlier in the request (e.g., `$TOKEN`).
- Numeric and string literals (quoted or unquoted when numeric).
- Response data directly via the capture syntax used elsewhere. For example: `^ & body.token == 'abc'` or `^ &[Login].header.authorization ~= /Bearer/`.

Optionally append `|| "custom message"` to override the default failure text. Messages may interpolate collection or request variables via `{{ variable }}` before evaluation.

### Callbacks

Callbacks are shell commands that are executed after a request is made. Callbacks are defined in the request definition with the `!` character. For example,

```text
GET https://lorem-api.com/api/user

# inline shell command
! echo "Request completed."

# a shell script
! sh ./post_request.sh
```

If a request has multiple callbacks, they are executed in the order they are defined, top to bottom.

```text
GET https://lorem-api.com/api/user

# This is executed first
! echo '1'

# This is executed second
! echo '2'
```

#### Callback Execution Context

Callbacks are executed with response data passed as environment variables.  The following environment variables are available to callbacks:

- `STATUS`: The HTTP status code of the response.
- `STATUS_TEXT`: The canonical reason phrase for the HTTP status code, when available.
- `RESPONSE`: The response body of the request.
- `DESCRIPTION`: The description of the request.

For example, the following callback will assert that the status code of the response is 200.

```bash
#!/bin/bash
# ./post_request.sh

if [ "$STATUS" -eq "200" ]; then
    echo "✅ [$DESCRIPTION] Received status 200"
    echo $result
else
    echo "❌ [$DESCRIPTION] Expected status 200 but got $STATUS"
    echo $result
fi
```

```text
Echo body w. callback

POST https://lorem-api.com/api/health

! sh ./post_request.sh
```

## Defining an API Collection

A file containing multiple requests is called a collection.  Collections can be used to group related requests together.

Collections can start with a preamble that contains metadata about the collection. The preamble is separated from the requests by a line containing three dashes `---`.  The same line is also used to separate requests from each other.

```text
name = Optional Collection Name
description = Optional Collection Description

[VARIABLES]

[GLOBAL HEADERS]

[GLOBAL QUERIES]

[GLOBAL CALLBACKS]

---

[request 1]

---

[request 2]

---

etc.

```

### Global Headers, Queries and Callbacks

Any headers, queries or callbacks defined in the collection preamble become global and are included in all requests in the collection.

In the example below, the `Authorization` header and `page` query is included in all requests in the collection.  When each request is executed and a response received, the callback `echo "Request completed."` is executed.

```text
* Authorization = foo
? page = 2
! echo "Request completed."
---
GET https://api.example.com/users
---
GET https://api.example.com/posts
```

> Global callbacks are executed before request-specific callbacks.

### User Prompts in Collections

User prompts can be used in a collection preamble. The user is prompted when the collection is loaded: either directly via the CLI or as a prompt in the interactive mode.

```text
$ foo = [[ bar ]]
---
POST https://lorem-api.com/api/echo

~~~ application/json
{ "foo" : "{{ foo }}" }
~~~
```

Prompt values can be supplied from the CLI using the `--input` flag, as described in the [User Prompts in Requests](#user-prompts-in-requests) section.

### Global Variables

Global variables are key/value pairs defined in the preamble of a collection file with the `$` character. For example,

<!-- markdownlint-disable MD014 -->
```text
$ api_origin = https://example.com
$ api_key = abc123
$ username = alice
```

Variables can be used in the request definition by enclosing the variable name in double curly braces. For example,

```text
GET {{ api_origin }}/todos/2

* Authorization = {{ api_key }}

? username = {{ username }}
```

Variables can also be set dynamically by running a shell command. For example,

```text
$ api_key = $(./get_secret.sh)
$ username = $(echo $USER)
```

Or by setting the variable interactively:

```text
$ api_key = [[ api_key ]]
```

<!-- markdownlint-enable MD014 -->

#### Request-Level Variables

Variables can also be declared inside an individual request. These request-level variables use the same `$ name = value` syntax and may appear before the HTTP method line or intermixed with other request statements. When both a global variable and a request variable share the same key, the request variable takes precedence for that request only.

```text
$ api_origin = https://lorem-api.com/api
---
Fetch banner image

$ banner_text = Promo%20Time!
$ banner_fill = 444444

GET {{ api_origin }}/image?text={{ banner_text }}&fill={{ banner_fill }}
```

Request variables fully support shell substitutions (`$(...)`) and interactive prompts (`[[ ... ]]`), and are resolved using the same working directory as the collection. This makes it easy to specialize a base request without modifying the collection preamble or other requests.

### Request Dependencies

Requests can depend on other requests in the same collection. Declare dependencies with the `>` directive placed before the HTTP method (or alongside other request-level declarations):

```text
> requires: Get JWT

GET https://api.example.com/profile
```

Hen constructs a directed acyclic graph (DAG) from these declarations when it loads the collection. Each dependency must reference another request's description exactly, and any cycles cause an error. When you execute a request, Hen automatically runs its prerequisites first—even if you selected only a single request from the CLI.

All variables captured or exported by a dependency become part of the dependent request's context. That means placeholders like `{{ TOKEN }}` resolve using values captured upstream, and captures can reference dependency responses via `&[Dependency].body...` as described earlier. This makes multi-step workflows (login → reuse token → perform action) straightforward to model in one collection file.

## Additional Syntax

### Comments

Comments are lines that are ignored by the parser. Comments start with the `#` character. For example,

```text
# This is a comment

GET https://example.com/todos/2
```

### Fragments

Fragments are reusable blocks of text that can be included in multiple requests. Fragments are defined in a separate file and included in a request with the `<<` character. For example,

```text
# .fragment.hen

* Authorization = abc123
```

```text
GET https://example.com/todos/2
<< .fragment.hen
```

Fragment paths can be absolute or relative to the collection file.

Fragments can contain multiple requests, headers, query parameters, and request bodies. Fragments can also contain variables and other fragments.

## Additional Features

### Export Requests

Requests can be exported as curl commands.  This is useful for debugging or sharing requests with others.

```text
 $ API_URL = https://lorem-api.com/api

---

POST {{ API_URL }}/echo

~~~ application/json
{ "foo" : "bar" }
~~~
```

```bash
curl -X POST 'https://lorem-api.com/api/echo' -H 'Content-Type: application/json' -d ' { "foo" : "bar" }'
```

Exporting happens once all variables and prompts have been resolved and the request is ready to be executed. Callbacks are ignored during export.

### Benchmarking

Requests can be benchmarked by specifying the `--benchmark` flag with the number of iterations to run.  This will run the request the specified number of times and output the average time taken to complete the request.

```bash
hen /path/to/collection_file.hen --benchmark 10
```

```text
Benchmarking request: Echo form data
[##################################################] 100.00%

Mean Duration: 399.95937ms
Variance (%): 0.6831308901940525
```

Notes:

- Callbacks are ignored when benchmarking.
- User prompts will still be executed in benchmarked requests, and so should be avoided, or used in the preamble only.

## Examples

### Basic Request

```text
GET https://lorem-api.com/api/user/foo
```

### Request with Headers, Query Parameters, and Form Data

```text
POST https://lorem-api.com/api/echo

# Header
* foo = abc123

# Query
? bar = abc123

# Form Data
~ baz = abc123
```

### Request with Body

```text
POST https://lorem-api.com/api/jwt

~~~ application/json
{
  "username": "bar",
  "password": "qux"
}
~~~
```

### Request with Callback

```bash
#!/bin/bash

if [ "$STATUS" -eq "200" ]; then
    echo "✅ [$DESCRIPTION] Received status 200"
else
    echo "❌ [$DESCRIPTION] Expected status 200 but got $STATUS"
fi
```

```text
GET https://lorem-api.com/api/user

! sh callback.sh
```

### Request with Assertions

```text
$ IMAGE_TEXT = https%3A%2F%2Florem-api.com
$ FILL_COLOR = CECECE

GET {{ API_URL }}/image?format=png&fill={{FILL_COLOR}}&text={{IMAGE_TEXT}}

& header.content-type > $IMAGE_CONTENT_TYPE

^ STATUS == 200
^ $IMAGE_CONTENT_TYPE == "image/png"

```

### Request with Dependencies

```text
$ API_URL = https://lorem-api.com/api

---

Get JWT

POST {{ API_URL }}/jwt

~~~ application/json
{
  "username": "foo",
  "password": "[[ password ]]"
}
~~~

& body.token > $JWT_TOKEN

^ STATUS == 200
^ $JWT_TOKEN ~= /[A-Za-z0-9_.-]+/

---

Echo Response

> requires: Get JWT

POST {{ API_URL }}/echo

~~~ application/json
{ "foo" : "{{ JWT_TOKEN }}" }
~~~

& body.foo > $ECHOED_TOKEN

^ ECHOED_TOKEN == $JWT_TOKEN
```