# Hen
Run API requests as files from the command line.
[Read Documentation](https://hen-api.com)
Hen keeps request definitions, assertions, captures, dependencies, and protocol-specific behavior in a single `.hen` file. It works well for local exploration, CI, and editor integrations.
## Table of Contents
- [Quick Example](#quick-example)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [CLI](#cli)
- [Authoring Model](#authoring-model)
- [Examples](#examples)
## Quick Example
```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
$ ROLE = [admin, user, guest]
---
Some descriptive title for the prompt.
POST {{ API_ORIGIN }}/echo
* Authorization = {{ API_KEY }}
? query_param_1 = value
~~~ application/json
{
"username": "{{ USERNAME }}",
"password": "[[ password ]]",
"role": "{{ ROLE }}"
}
~~~
^ & status == 200
! sh ./callback.sh
```
This single file can define variables, prompt inputs, mapped requests, assertions, and callbacks. Add `> requires:` when later requests depend on earlier ones, and use response captures to thread values through a collection.
## Installation
```bash
cargo install hen
```
This installs the `hen` CLI.
### Container Image
The repository also ships a container image that includes the `hen` CLI.
Build it locally with Docker:
```bash
docker build -t hen:latest .
```
## Quick Start
Verify a collection without executing shell commands or network requests:
```bash
hen verify ./examples/lorem.hen
```
Run every request in a collection non-interactively:
```bash
hen run ./examples/lorem.hen all --non-interactive
```
Emit machine-readable output for scripts or CI:
```bash
hen run ./examples/lorem.hen all --non-interactive --output json
```
Inspect a collection's authoring structure without executing requests:
```bash
hen inspect ./examples/graphql_protocol.hen --output json
```
Import an OpenAPI contract into an editable `.hen` collection:
```bash
hen import ./examples/openapi_import.yaml --output ./examples/openapi_imported.hen
```
Run the same collection against a selected named environment:
```bash
hen run ./examples/environment_overrides.hen 0 --env local --non-interactive
```
Verify a collection that references local secret providers without loading the secrets:
```bash
hen verify ./examples/local_secrets.hen
```
## CLI
Hen has four primary commands:
- `hen run` executes a collection or request.
- `hen verify` parses and validates a collection without making requests.
- `hen inspect` exposes machine-readable authoring structure for editor tooling and analysis.
- `hen import` lowers an OpenAPI 3.x JSON or YAML spec into an editable `.hen` collection.
The default `hen [PATH] [SELECTOR]` form still works for interactive terminal use, but `hen run ...` is the clearer choice for CI and automation.
Use `hen inspect --output json` when another tool needs collection summaries, local declaration and import ranges, request metadata, required inputs, and merged symbol tables without running the collection.
### OpenAPI import
Use `hen import <spec-path> [selector] --output <collection.hen>` when you want a contract-first starting point from an OpenAPI 3.0.x or 3.1.x spec.
- Omit the selector or use `all` to import every safely materializable operation.
- Select one operation with an `operationId`, `METHOD /path`, `tag:name`, or numeric index.
- Supported schema lowering includes plain component aliases, object-shaped `allOf`, named `oneOf(...)` and `anyOf(...)` unions, scalar `const(...)` tags, and `discriminator(...)` unions when the OpenAPI schema shape is representable in Hen.
- Import fails when the spec is invalid, references cannot be resolved, or the selected operation set cannot be emitted as valid `.hen`.
- Successful imports still print warnings for approximations such as reduced auth flows or omitted unsupported schema details.
See [examples/openapi_import.yaml](examples/openapi_import.yaml) for the real API source contract and [examples/openapi_imported.hen](examples/openapi_imported.hen) for its generated collection. See [examples/openapi_union_import.yaml](examples/openapi_union_import.yaml) and [examples/openapi_union_imported.hen](examples/openapi_union_imported.hen) for a focused union and discriminator import example.
### Selection and prompts
- If a directory contains one `.hen` file, Hen selects it automatically.
- If a collection contains multiple requests, provide an index or `all` to bypass the picker.
- The text CLI prompts for unresolved `[[ prompt ]]` placeholders.
- `--non-interactive` disables selection prompts and fails when required prompt values are missing.
- Use repeated `--input key=value` flags to provide prompt values up front.
- Use `--env <name>` to apply a named collection environment before request execution.
### Named environments
Collections can declare named environment overlays in the preamble to swap scalar variable values without editing requests.
```text
name = Example Collection
$ API_ORIGIN = https://api.example.com
$ CLIENT_ID = [[ client_id ]]
env local
$ API_ORIGIN = http://localhost:3000
$ CLIENT_ID = hen-local
---
Get profile
GET {{ API_ORIGIN }}/profile
* X-Client-Id = {{ CLIENT_ID }}
```
- Environment blocks are only valid in the preamble before the first `---`.
- Environment overrides can only target previously declared scalar variables in this milestone.
- If no environment is selected, Hen keeps the collection-level defaults.
- `hen verify` reports available environment names without selecting one.
- Structured run output reports the selected environment so CI artifacts show which overlay was used.
Resolution order is:
1. Collection preamble scalar assignments.
2. The selected named environment.
3. Explicit `--input key=value` values for prompt placeholders.
4. Prompt defaults declared with `[[ name = default ]]`.
5. Runtime dependency captures and callback exports for downstream requests.
### Local secret providers
Hen supports local secret references inside scalar assignments.
<!-- markdownlint-disable MD014 -->
```text
$ API_TOKEN = secret.env("HEN_API_TOKEN")
$ CLIENT_ID = secret.file("./secrets/client_id.txt")
```
- `secret.env("NAME")` reads one environment variable at run time.
- `secret.file("PATH")` reads one 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 only resolved during runs, not during `hen verify`.
- Secret reference arguments are string literals in this slice; interpolation inside `secret.env(...)` or `secret.file(...)` is intentionally out of scope.
- Supported providers in this slice are `env` and `file`. OS keychain remains a stretch goal and is not implemented.
### Safe output redaction
Hen automatically redacts values loaded through `secret.*(...)` plus built-in sensitive header names such as `Authorization`, `Proxy-Authorization`, `Cookie`, `Set-Cookie`, and API-key style headers across text, JSON, NDJSON, JUnit, transcripts, and retained artifacts.
Use preamble redaction rules to broaden that default policy without changing request execution:
```text
redact_header = X-Session-Token
redact_capture = SESSION_ID
redact_body = body.session.accessToken
```
- `redact_header = NAME` adds one header name to the built-in masked header set.
- `redact_capture = NAME` treats captured or exported values under that name as sensitive in the current request and in downstream dependent requests that reuse them.
- `redact_body = body.path` or `redact_body = json(body.payload).token` masks a specific response-body value even when you do not export it.
- Redaction rules are additive, only valid in the collection preamble before the first `---`, and are validated structurally by `hen verify`.
- See [examples/redaction_rules.hen](examples/redaction_rules.hen) for a concrete collection.
### HTTP session cookies
Ordinary HTTP requests can author explicit outbound cookies with `@ ...` and can reuse response-set
cookies by sharing the same `session = ...` handle.
```text
name = Cookie Example
@ region = us-east-1
---
Load dashboard
session = web
GET https://example.com/dashboard
@ session = [[ session_id ]]
```
- `@ cookie_name = value` is valid in the collection preamble and inside individual requests.
- Request-level cookies override preamble cookies when the cookie name matches exactly.
- Hen serializes structured `@ ...` cookie directives 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.
Session-backed requests still reuse the same cookie jar for the duration of the run:
```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"
```
- Requests that share the same HTTP session reuse one cookie jar for that run.
- Session reuse also adds the same implicit ordering Hen already uses for other session-backed
protocols, so login-before-profile flows stay deterministic.
- Built-in redaction already masks `Cookie` and `Set-Cookie` headers across text, JSON, NDJSON,
JUnit, transcripts, and retained artifacts.
- See [examples/http_cookie_session.hen](examples/http_cookie_session.hen) for a concrete
collection.
### OAuth profiles
Hen can acquire and reuse OAuth tokens from named preamble profiles.
```text
name = OAuth Client Credentials
$ API_ORIGIN = https://api.example.com
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 = {{ API_ORIGIN }}
access_token -> $API_ACCESS_TOKEN
token_type -> $API_TOKEN_TYPE
---
Get profile
auth = api
GET {{ API_ORIGIN }}/me
* X-Token-Type = {{ API_TOKEN_TYPE }}
```
- `oauth <name>` blocks are only valid in the collection preamble before the first `---`.
- Requests attach a profile with `auth = <name>` and Hen lazily acquires or reuses the token immediately before dispatch.
- Supported grants in this slice are `client_credentials` and `refresh_token`.
- Use exactly one endpoint source per profile: `issuer = ...` for OIDC discovery or `token_url = ...` for a direct token endpoint.
- `param name = value` adds extra token form fields such as `audience` or `resource`.
- `<field> -> $VARIABLE` maps token-response fields into ordinary scalar exports that the current request and downstream dependent requests can reuse.
- Hen injects `Authorization: Bearer ...` automatically unless the request already sets `Authorization` explicitly.
- Discovery and token acquisition only happen during `hen run`, never during `hen verify`.
- Access tokens, refresh tokens, and injected authorization headers stay redacted across text output, structured reports, transcripts, and retained artifacts.
- See [examples/oauth_client_credentials.hen](examples/oauth_client_credentials.hen) and [examples/oauth_refresh_token.hen](examples/oauth_refresh_token.hen) for concrete collections.
### Conditional guards
Use a bracketed predicate to conditionally run an assertion or include a fragment.
```text
[ USERNAME == "foo" ] ^ & body.username == "foo"
[ FEATURE_ENABLED != true ] << ./fragments/legacy_assertions.hen
```
- Guards can prefix assertions or `<<` fragment imports.
- A false assertion guard skips that assertion without failing the request.
- A false fragment guard skips that import entirely.
- Guard predicates use the same comparison and regex operators as ordinary assertions, except schema validation with `===` is not supported in guards.
- See [examples/conditionals.hen](examples/conditionals.hen) for a runnable assertion-guard example.
### Useful options
- `--body none|selected|failed|all` controls how much of the request and response bodies appear in text output and retained artifacts.
- `--output text|json|ndjson|junit` selects human or machine-readable output.
- `--parallel` runs independent requests concurrently.
- `--max-concurrency N` throttles parallel execution.
- `--continue-on-error` keeps unaffected dependency branches running.
- `--benchmark N` benchmarks a request instead of running it once.
- `--export` renders the request as a curl command.
- `--verbose` includes more detail in text output and enables debug logging.
### Schema validation
Hen supports collection-local `scalar` and `schema` declarations plus built-in targets such as
`UUID`, `EMAIL`, `NUMBER`, `DATE`, `DATE_TIME`, `TIME`, and `URI`.
```text
scalar HANDLE = string & len(3..24) & pattern(/^[a-z][a-z0-9_]*$/)
scalar CardKind = const("card")
schema User {
id: UUID
email: EMAIL
handle: HANDLE
}
schema Users = User[]
schema Contact = anyOf(EMAIL, URI)
schema Checkout = discriminator(method,
"card": CardCheckout,
"bank": BankCheckout
)
```
- `scalar` declarations can start from a primitive type, a built-in scalar target, or another
named scalar, then refine that value with `const(...)`, `enum(...)`, `format(...)`, `len(...)`,
`pattern(...)`, and `range(...)` predicates.
- `schema` declarations support object shapes, plain aliases, root-array aliases, and combinators
such as `allOf(...)`, `oneOf(...)`, `anyOf(...)`, `not(...)`, and `discriminator(...)`.
- Object schema fields are open by default in v1. Use `field?: Type` for optional fields and
`Type?` for nullable values.
- Scalar and schema references are resolved after the full preprocessed collection is loaded, so
forward references are valid across the preamble and imported fragments.
- `===` validates a typed JSON value against a built-in scalar target, named scalar declaration, or
named schema declaration.
For the full declaration grammar and more examples, see [syntax-reference.md](syntax-reference.md),
[examples/openapi_union_imported.hen](examples/openapi_union_imported.hen), and the schema examples
under [examples/](examples/).
### Assertion labels
Use a `# ...` comment directly above an assertion to annotate it with a short intent label:
```text
# The page loads
^ & status == 200
```
- Successful text output prints the label, for example `✅ [Fetch fixture] [The page loads]`.
- The comment labels only the assertion on the very next line.
- Blank lines or other request items break the association.
- Labels are literal text and are not interpolated.
## Authoring Model
Hen files are plain text collections made of a preamble and one or more requests separated by `---`.
### Core concepts
- Variables: `$ NAME = value`, shell substitutions with `$(...)`, local secret references such as `secret.env("NAME")` and `secret.file("./path")`, prompt placeholders with `[[ name ]]`, and simple arrays for mapped requests.
- Named environments: `env <name>` blocks in the preamble override previously declared scalar variables for a selected run.
- OAuth profiles: preamble `oauth <name>` blocks plus request-level `auth = <name>` provide lazy token acquisition and mapped token exports.
- Redaction rules: preamble-level `redact_header`, `redact_capture`, and `redact_body` extend the default safe-output masking policy without changing request behavior.
- Request inputs: headers with `*`, cookies with `@`, query parameters with `?`, form data with `~`, and fenced body blocks.
- Requests: HTTP by default, plus explicit `protocol = graphql`, `protocol = mcp`, `protocol = sse`, and `protocol = ws`.
- Reliability: preamble and request-level `timeout`, `poll_until`, and `poll_every` directives control per-attempt timeouts and eventual-consistency polling.
- Captures: `& body.token -> $TOKEN` stores response data for later requests, assertions, and callbacks.
- Assertions: `^` lines validate status, headers, body fields, structural JSON matches, and schema targets.
- Dependencies: `> requires: Request Name` creates a DAG so setup requests run before dependents.
- Fragments: `<< file.hen` reuses shared request snippets or declarations.
- Callbacks: `!` lines run shell commands after request execution.
- Guards: `[predicate]` can gate assertions or fragment imports.
### JSON selection and queries
Hen uses a small, explicit JSON selector syntax for captures, assertions, dependency reads, and body redaction.
```text
& body.user.id -> $USER_ID
& body.token -> $TOKEN := fallback
^ & body.items[0].name == "first"
^ & body.[0].id == 123
^ & body.jobs[? recipient == $RECIPIENT && status != "failed"].status == "succeeded"
^ & json(body.result.content[0].text).items[0].id == "123"
^ &[Create Job].body.result.state == "completed"
```
- Use `body.field` for object-root responses and `body.[0]` when the root is an array.
- Use `[index]` to step into arrays and `[? ...]` to query an array by field values.
- Filter queries are evaluated relative to each array item, support `==`, `!=`, scalar literals, `$VARIABLE` values, and `&&`, and must identify exactly one match.
- Use `json(...)` when a selected field contains stringified JSON that needs another traversal pass.
- Captures may include a fallback with `:=`, for example `& body.token -> $TOKEN := fallback`, which is used when the selected path is missing.
- Hen does not implement full JSONPath or jq features such as wildcards, projections, recursive descent, slices, regex filters, or `||`.
- See [examples/json_response_captures.hen](examples/json_response_captures.hen) and [examples/filtered_selector_variables.hen](examples/filtered_selector_variables.hen) for runnable examples.
### Request reliability
```text
timeout = 30s
poll_every = 1s
---
Create export
POST https://api.example.com/exports
timeout = 20s
^ & status == 202
& body.jobId -> $JOB_ID
---
Wait for export
GET https://api.example.com/exports/{{ JOB_ID }}
timeout = 5s
poll_until = 2m
poll_every = 2s
^ & status == 200
^ & body.state == "completed"
^ & body.downloadUrl != null
```
- `timeout` applies to each attempt and defaults to `30s`.
- `poll_until` is off by default. When set, Hen reruns the same request until its assertions pass or the poll window expires.
- `poll_every` controls the fixed retry interval and defaults to `1s` whenever polling is enabled.
- Preamble directives provide collection-wide defaults; a request overrides only the fields it sets.
- Polling retries only assertion failures and per-attempt timeouts. Transport failures stay terminal.
- Durations use `ms`, `s`, or `m` suffixes such as `250ms`, `2s`, and `1m`.
- `within = ...` remains the protocol-specific receive or exchange timeout for SSE and WebSocket steps; use `timeout = ...` for ordinary request execution.
- See [examples/request_reliability.hen](examples/request_reliability.hen) for a complete authoring example.
### Protocol support
- HTTP: ordinary request and response workflows.
- GraphQL: GraphQL-over-HTTP with `operation`, `variables`, and `~~~graphql` documents.
- MCP: MCP-over-HTTP authoring with generated JSON-RPC envelopes and reusable sessions.
- SSE: named streaming sessions with `receive` steps and `within` timeout windows.
- WebSocket: `open`, `send`, `exchange`, and `receive` flows over a named session.
### Full syntax guide
The complete authoring grammar lives in [syntax-reference.md](syntax-reference.md). That file covers:
- variables and prompts
- headers, cookies, query parameters, form data, and body blocks
- JSON selection, filtered array queries, and decoded `json(...)` traversal
- reliability directives and protocol-specific directives
- declarations, fragments, captures, assertions, guards, callbacks, and dependencies
## Examples
The fastest way to learn the format is to run the included examples:
- [examples/lorem.hen](examples/lorem.hen): basic HTTP requests
- [examples/environment_overrides.hen](examples/environment_overrides.hen): named environment overlays
- [examples/oauth_client_credentials.hen](examples/oauth_client_credentials.hen): client-credentials auth with mapped token fields
- [examples/oauth_refresh_token.hen](examples/oauth_refresh_token.hen): refresh-token auth with downstream reuse
- [examples/conditionals.hen](examples/conditionals.hen): conditional assertions with guard predicates
- [examples/local_secrets.hen](examples/local_secrets.hen): local env and file secret providers
- [examples/redaction_rules.hen](examples/redaction_rules.hen): additive safe-output redaction rules
- [examples/request_reliability.hen](examples/request_reliability.hen): per-attempt timeouts and poll-until authoring
- [examples/openapi_import.yaml](examples/openapi_import.yaml): source OpenAPI contract for import workflows
- [examples/openapi_imported.hen](examples/openapi_imported.hen): generated `.hen` output from the OpenAPI import example
- [examples/openapi_union_import.yaml](examples/openapi_union_import.yaml): source OpenAPI contract showing `oneOf` and `discriminator` import
- [examples/openapi_union_imported.hen](examples/openapi_union_imported.hen): generated `.hen` output for union and discriminator import
- [examples/json_response_captures.hen](examples/json_response_captures.hen): captures and JSON paths
- [examples/filtered_selector_variables.hen](examples/filtered_selector_variables.hen): selector variables in captures and assertions
- [examples/schema_scalar_checks.hen](examples/schema_scalar_checks.hen): scalar validation
- [examples/schema_object_validation.hen](examples/schema_object_validation.hen): object schema validation
- [examples/schema_root_array_validation.hen](examples/schema_root_array_validation.hen): root-array schemas
- [examples/schema_fragment_reuse.hen](examples/schema_fragment_reuse.hen): declaration reuse through fragments
- [examples/structural_json_matching.hen](examples/structural_json_matching.hen): structural JSON assertions
- [examples/graphql_protocol.hen](examples/graphql_protocol.hen): GraphQL authoring
- [examples/mcp_protocol.hen](examples/mcp_protocol.hen): MCP-over-HTTP sessions
- [examples/sse_protocol.hen](examples/sse_protocol.hen): server-sent events
- [examples/ws_protocol.hen](examples/ws_protocol.hen): WebSocket sessions