A markdown-native REST client. Requests live in .md files — version-controlled,
diffable, and executable. See spec.md for the full design.
demo.mp4
# macOS / Linux
curl -LsSf https://github.com/jamiedavenport/restmd/releases/latest/download/restmd-installer.sh | sh
# Homebrew (macOS / Linux)
brew install jamiedavenport/tap/restmd
# Windows (PowerShell)
powershell -c "irm https://github.com/jamiedavenport/restmd/releases/latest/download/restmd-installer.ps1 | iex"Update later with restmd-update, brew upgrade, or by re-running the installer.
The restmd binary also bundles the language server (restmd lsp), so editor
support needs nothing extra — see editors/ for setup.
restmd init # scaffold ./.restmd with an example request
restmd # open the TUI on ./.restmdWhen run from a detected coding-agent environment, restmd init also creates
scoped agent context under .restmd/: a portable AGENTS.md plus only the
detected agent's adapter files.
Navigate with Tab/h/l and j/k, press Enter to run a request (and the
earlier ones it depends on), o to open the current file in $EDITOR, q to
quit. Editing a file under .restmd/ refreshes the TUI live. A runnable
playground lives in demo/.
restmd run sends requests headlessly and reports the results — no TUI, script-
and pipeline-friendly. With no path it runs every .md file in ./.restmd;
pass files or directories to narrow it down.
restmd run # run every file in ./.restmd
restmd run demo/.restmd # run a directory
restmd run demo/.restmd/auth.md # run one fileThe exit code is what CI keys off: 0 success, 1 assertion failure, 2 parse
error, 3 network error, 4 config error. The most severe wins when several
files run.
--format pretty(default, colored in a terminal),json(a machine-readable report), orjunit(XML test report for CI to ingest).--env NAMEselects anenvironmentsblock from the frontmatter;--var k=v(repeatable) overrides individual variables.-r REQUESTruns a single request — by 1-based index or a substring of its heading. The earlier requests it depends on still run (so captures resolve), but only the selected one is reported.
Responses that set cookies automatically feed those cookies into later
requests in the same file run, following normal domain, path, expiry, and
Secure rules. Cookie sessions are in memory only and reset for each file and
each later TUI run. An explicit Cookie request header takes precedence over
the session cookie store.
restmd run demo/.restmd --env prod --format junit > results.xml
restmd run demo/.restmd/users.md -r "POST /users" --var id=42The bundled language server (restmd lsp) brings completion, diagnostics,
document symbols, and hover to .restmd/ files. It engages only inside
.restmd/ directories, so your files still render as plain markdown elsewhere.
- VS Code — install the restmd extension from the Marketplace.
- Zed, Neovim, Helix — see
editors/for setup.
Each editor shells out to restmd lsp, so just keep the restmd binary on your
PATH (the install step handles that) — nothing else to install.
A restmd file is an ordinary markdown .md file (kept in a .restmd/
directory). It has three layers: an optional frontmatter block of file-level
config, free-form prose that the runner ignores, and one or more
requests. Here is a complete file:
---
base: http://127.0.0.1:8787
defaults:
Accept: application/json
---
# Auth flow
Log in, capture the token, then make an authenticated request that depends on
it. Running the second request runs the first one too.
## POST /auth/login
Content-Type: application/json
```json
{ "email": "ada@example.com", "password": "hunter2" }
```
> capture token = $.access_token
> capture userId = $.user.id
> assert status == 200
> assert $.access_token exists
## GET /users/{{userId}}
Authorization: Bearer {{token}}
> assert status == 200
> assert $.active == trueAn optional YAML block at the very top, fenced by ---. Unknown keys are
rejected, so typos surface as errors. Recognized keys:
| Key | Meaning |
|---|---|
base |
Base URL prepended to relative request paths (may be templated). |
defaults |
Headers merged into every request unless the request overrides them. |
environments |
Named variable sets, e.g. dev: / prod:, selectable per run. |
openapi |
Path or URL of an OpenAPI spec, for completion/validation. |
timeout |
Default per-request timeout, e.g. 30s. |
retries |
Default retry count for idempotent methods. |
Each request is an H2 heading of the form ## METHOD /path, where METHOD is
one of GET, POST, PUT, PATCH, DELETE, HEAD, or OPTIONS. An H2 whose
first word isn't a method is treated as prose, not a broken request. The path is
relative to base and may carry a query and fragment. A request runs from its
heading until the next H2, the next H1, or end of file.
Directly under the heading, optional header lines (Name: value) and an
optional body fence make up the request:
```json— parsed and re-sent; defaultsContent-Type: application/json.```xml— sent as-is; defaultsContent-Type: application/xml.```form—key: valuelines sent as form data.```text— sent verbatim (setContent-Typeyourself).```graphql— wrapped into a GraphQL JSON payload.
A fence in any other language (e.g. ```rust) is left as prose, so
documentation snippets don't get sent.
Lines beginning with > (markdown blockquotes) attach to the request above
them. They run after the response comes back:
> capture NAME = $.json.path— save a value for later requests. The source can also beresponse.headers.HeaderNameorresponse.status.> assert status == 200— assert on the status code.> assert $.json.path OP value— assert on the body. Operators:==,!=,<,>,<=,>=,exists, andmatches /regex/.> set NAME = value— bind a variable without sending a request.
{{name}} interpolates a variable anywhere in a path, header, or body. Lookup
is first-match-wins in this order:
- Values
captured (orset) by earlier requests in the run. --var name=valueflags passed torestmd run.RESTMD_VAR_<NAME>environment variables.- The selected
environmentsblock from the frontmatter.
Two modifiers help with missing values: {{name?}} resolves to an empty string
instead of erroring, and {{name!fallback}} uses fallback when unset.
Built-in functions are also available: {{uuid()}}, {{now()}},
{{timestamp()}}, {{base64(var)}}, and {{env(NAME)}} (reads NAME straight
from the OS environment).
Building from source, tests, and releases are covered in
CONTRIBUTING.md.
