Skip to content

Syntax

This is the language reference for the .httui block format — what httui-lang parses, what httui-lsp validates, what the tree-sitter grammar lights up.

A .httui file is a sequence of blocks, separated by blank lines. Each block opens with a fence line, then has a body, optionally followed by an expect section.

<block> ::= <fence-line> NEWLINE
<body>
( "# expect:" NEWLINE <expect-line>+ )?
<fence-line> ::= ( "http" | "db-" CONN-NAME ) <info-token>*

A .md runbook embeds the same syntax inside fenced code blocks:

```http alias=login
POST {{BASE_URL}}/auth/login
```

The opening ```http line plays the role of <fence-line>. The ``` closing line ends the body. Tree-sitter handles both modes (standalone .httui and embedded .md).

After the language word (http or db-<conn>), additional tokens configure the block. Whitespace-separated key=value pairs.

http alias=login timeout=5000 display=split mode=raw
^^^^ ^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^
lang alias timeout display mode
TokenValue typeDefaultMeaning
aliasidentifiernoneblock name (required for chain)
timeoutint (ms)30000 HTTP / 60000 DBrequest timeout
displayinput | output | splitspliteditor panel layout
moderaw | formrawbody editor mode (HTTP only)
connectionstringDB only; alternative to db-<name> fence (rare)

Canonical order for serialisation: alias → timeout → display → mode. The httui editor reformats to this order on save.

<alias> and <connection-name> follow standard identifier rules:

ident ::= [a-zA-Z_] [a-zA-Z0-9_-]*

Letters, digits, underscores, hyphens. No spaces, no dots, no $.

What follows the fence line until the closing fence (or EOF in a .httui file).

HTTP-body ::= <request-line> NEWLINE
( <header-line> NEWLINE )*
( NEWLINE <message-body> )?

Request line:

GET https://api.example.com/users/{{id}}
^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
verb URL (with {{ref}} interpolation)

Verb is one of GET | POST | PUT | PATCH | DELETE | HEAD | OPTIONS. URL is any string, including {{ref}} expressions, until end of line.

Headers: standard HTTP header lines (Name: value). One per line. Values can contain {{ref}}.

Body: anything after the first blank line, until the close. The Content-Type header tells the runtime how to parse it; the language doesn’t care.

DB-body ::= <sql-statement> ( ";" <sql-statement> )*

Free-form SQL. Multi-statement allowed (separated by ;). {{ref}} inside SQL is converted to a bind parameter by the runtime — the language parses it as a reference expression, but the executor never string-interpolates.

After the body, an optional # expect: section turns the block into an assertion.

<expect-section> ::= "# expect:" NEWLINE
( "#" <assertion> NEWLINE )+
<assertion> ::= <field> <operator> <literal>
| <field> "exists"
| <field> "not" "exists"
| <field> "is" <type-name>
FieldSource
statusHTTP status
timetotal elapsed (ms unless suffixed)
sizeresponse body bytes
body.<path>JSON path into response body
headers.<name>response header (case-insensitive)
cookies.<name>set-cookie value
affected (DB)rows affected
row[N].<col> (DB)nth row column
rows.length (DB)row count
OpMeaning
==, !=strict equality / inequality
<, <=, >, >=numeric comparison
contains, not containsstring / array membership
matchesregex /.../
exists, not existspath resolves
istype check
time < 500ms time < 1.5s time < 1m
size < 10kb size < 100mb
SuffixMeans
ms s mtime
b kb mb gbbytes (powers of 1024)

The {{...}} expression — fully detailed in Block references, summary here.

<reference> ::= "{{" ( <alias-ref> | <env-ref> | <positional-ref> ) "}}"
<alias-ref> ::= IDENT "." <field> ( "." <path-segment> )*
<env-ref> ::= IDENT # no dots, looks up env vars
<positional-ref> ::= "$prev" ( "." <path-segment> )*
<path-segment> ::= IDENT | "[" INT "]" | "[" QUOTED-STRING "]"

Resolution priority: block alias > env var. If both have the same name, the block wins.

# this is a comment

A # at the start of a line is a comment, except inside the expect section where # introduces assertions. The tree-sitter grammar distinguishes them by context.

Inline comments (mid-line) are not supported. End-of-line # foo inside a URL is treated as part of the URL.

  • Trailing whitespace on lines is ignored.
  • A blank line inside the body separates headers from body (HTTP).
  • A blank line between blocks separates blocks (in .httui files).
  • In .md runbooks, the fence delimiters override blank-line rules (the body of a fence is everything until the closing fence).

These keys can’t be used as alias values:

alias timeout display mode connection

Other identifiers are free.

For tree-sitter and parser implementers — the full grammar:

file ::= block ( BLANK-LINE+ block )*
block ::= fence-line NEWLINE body expect-section?
fence-line ::= ( "http" | "db-" ident ) info-token*
info-token ::= info-key "=" info-value
info-key ::= "alias" | "timeout" | "display" | "mode" | "connection"
info-value ::= ident | int | "raw" | "form" | "input" | "output" | "split"
body ::= http-body | db-body
http-body ::= request-line NEWLINE
( header-line NEWLINE )*
( BLANK-LINE message-body )?
request-line::= verb SP url
verb ::= "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"
db-body ::= sql-statement ( ";" sql-statement )*
expect-section ::= "# expect:" NEWLINE
( "#" SP assertion NEWLINE )+
assertion ::= field op literal
| field "exists"
| field "not" SP "exists"
| field "is" SP type-name
reference ::= "{{" ( alias-ref | env-ref | "$prev" path? ) "}}"
alias-ref ::= ident "." field-name path?
env-ref ::= ident
path ::= ( "." ident | "[" int "]" | "[" QUOTED "]" )+
ident ::= [a-zA-Z_] [a-zA-Z0-9_-]*

(Simplified — tree-sitter source is the canonical version.)