Skip to content

CLI — restura-cli

restura-cli runs your collections headlessly with the same protocol implementations as the desktop and web apps. Drop it into CI to assert your API, get JUnit XML for your dashboard, ship an HTML report to your team.

Live dashboard — restura run

flowchart LR
  CI[CI runner] -->|exec| CLI[restura run]
  CLI -->|load| COL[(OpenCollection<br/>dir / .yaml)]
  CLI -->|exec| H[HTTP / GraphQL]
  CLI -->|exec| G[gRPC Connect]
  CLI -->|exec| S[SSE]
  CLI -->|exec| M[MCP]
  CLI -->|exec| W[WebSocket]
  H --> R1[tui · live · json · junit · html · stats]
  G --> R1
  S --> R1
  M --> R1
  W --> R1
  R1 -->|exit code| CI

  classDef cli fill:#7b6ef6,stroke:#4f46e5,color:#fff;
  classDef proto fill:#6366f122,stroke:#7b6ef6,color:#7b6ef6;
  class CLI cli;
  class H,G,S,M,W proto;
Terminal window
npm install -g restura-cli
# or, no install:
npx restura-cli run ./my-collection

Requires Node.js 24+.

Export a collection from the Restura app (File → Export → OpenCollection directory), then:

Terminal window
restura run ./my-collection --reporter junit --reporter-output junit=results.xml

Exit code is 0 when every request passed, 1 if any failed, 2 on internal errors (missing collection, bad flags).

Run restura (or restura run) with no collection in a terminal to launch a guided wizard — pick the collection, reporters, output paths, an optional env file, and whether to allow localhost — then it runs.

Interactive wizard

Terminal window
restura # bare → wizard
restura run # also → wizard when no collection is given

By default an interactive run shows the live tui dashboard; piped output and CI fall back to plain line output (live). See Reporters.

The loader auto-detects three layouts:

LayoutDetected when…
OpenCollection directory (preferred)the target directory contains opencollection.yml (or .yaml)
OpenCollection bundled filethe target path ends in .yaml / .yml
Legacy file-collection (deprecated)the target directory contains _collection.yaml

The legacy format prints a stderr deprecation warning the first time it’s loaded.

ProtocolNotes
HTTP / RESTFull support
GraphQLRuns as HTTP with body type graphql
gRPCVia Connect protocol (JSON-encoded, no proto compilation needed)
SSECaptures events for --sse-duration ms, or until --sse-events N
MCPSingle JSON-RPC POST per request
WebSocketExecutor available; not yet wired into the dispatcher
Terminal window
restura run [collection] [options]

[collection] accepts a directory (any supported layout) or a bundled .yaml / .yml file. Omit it in a terminal to pick one via the interactive wizard; in CI it is required.

FlagDefaultDescription
--env <file>JSON or YAML env file. ${VAR} placeholders are expanded from process.env.
--reporter <list>autoComma-separated: tui, live, json, junit, html, stats. Defaults to tui in a terminal, live otherwise (piped / CI).
--output <file>Shorthand for single file reporter.
--reporter-output <kv...>Per-reporter output: --reporter-output junit=results.xml html=report.html.
--bailfalseStop on first failure.
--timeout <ms>30000Per-request timeout.
--allow-localhostfalsePermit requests to localhost / 127.0.0.1. Off by default (SSRF guard).
--folder <path>Only run requests under this folder path (slash-joined).
--include <pattern...>Substring or glob (e.g. users/*). Repeatable.
--exclude <pattern...>Same syntax as --include. Applied after.
--data <file>CSV (with header row) or JSON array. Runs the collection once per row.
--max-iterations <n>Cap iterations when a --data file is large.
--retry <n>0Retry attempts per failing request.
--retry-on <list>network,5xxTriggers: network, 5xx, 4xx, or specific status codes (429,503).
--sse-duration <ms>5000How long to keep SSE streams open.
--sse-events <n>Stop SSE early after N events.
--insecurefalseSkip TLS certificate verification (self-signed / staging).
--ca <file>PEM CA bundle to trust (private CA) without disabling verification.
--client-cert <file>PEM client certificate for mutual TLS (mTLS).
--client-key <file>PEM client private key for mutual TLS.
--cert-passphrase <value>Passphrase for an encrypted --client-key.
--proxy <url>HTTP(S) proxy URL. Overrides HTTP_PROXY / HTTPS_PROXY; composes with the TLS flags.
Terminal window
restura workflow run <workspace> <workflow-id> [--env ci.env] [--timeout 30000]

The workspace must be an OpenCollection directory with opencollection.yml and a complete workflows/<workflow-id>/ artifact set. The command accepts only Restura’s safe OWS profile: do, set, wait, timeout/cancellation, and binding-only HTTP calls to saved requests. It rejects inline endpoints, credentials, non-HTTP calls, and controls without a bounded runtime before dispatching any network request.

Pre-request and test scripts run in a sandboxed QuickJS WASM VM — no DOM, no filesystem, no network escape; 10 MB memory cap, 5 s execution timeout. See Scripts for the full Postman API surface.

request.http.yaml
name: Get user
method: GET
url: "{{API_BASE}}/users/1"
testScript: |
pm.test("status is 200", () => pm.response.to.have.status(200));
pm.test("response has name", () => {
pm.expect(pm.response.json()).to.have.property("name");
});

When a test script runs and defines any pm.test(...) assertion, those drive pass/fail. Otherwise pass/fail falls back to the transport outcome (HTTP 2xx, gRPC OK, etc.).

Variables set inside a script (pm.environment.set('K', 'v')) propagate to subsequent requests in the same run.

Three layered sources, in order of precedence (later wins):

  1. --env file
  2. Collection variables (declared in opencollection.yml or _collection.yaml)
  3. Iteration row (when --data is set)

Substitutions use {{NAME}}. Unknown vars are left in place so the upstream sees them and you notice the gap.

Postman-compatible {{$random*}} / {{$timestamp}} helpers are expanded after user var substitution:

HelperExample
{{$randomUUID}}f4d2e3...
{{$timestamp}}1700000000
{{$isoTimestamp}}2026-05-22T13:42:00Z
{{$randomEmail}}alice.42@example.com
{{$randomFirstName}}Olivia
{{$randomIP}}192.0.2.4

Full list in shared/variables/dynamic.ts.

Terminal window
restura run ./users-api --data ./users.csv --reporter junit --reporter-output junit=junit.xml
users.csv
username,role
alice,admin
bob,viewer
charlie,editor

Each row exposes username and role as variables, overriding any same-named env or collection variable for that iteration only. Reporter output groups results by iteration index.

The default is chosen automatically: the live tui dashboard when stdout is a terminal (and NO_COLOR is unset), live lines otherwise (piped output / CI).

Live, in-place dashboard with a progress bar and a spinner on the in-flight request. The default in an interactive terminal; on completion it collapses to a clean failure list + summary.

The live reporter — also what CI logs get — prints one line per request:

Line reporter output

Combine with a comma: --reporter live,junit --reporter-output junit=results.xml.

CodeMeaning
0Every request passed AND at least one request ran
1One or more requests failed or errored (or no requests matched after filtering)
2Internal error: missing collection, bad reporter name, IO failure, …

restura agent eval <suite-or-bundle.json> runs a versioned Agent Suite or portable Agent Bundle in CI and prints a compact JSON summary. It exits 0 only when every trial and every configured baseline gate passes; evaluated failures, errors, and cancellations exit 1; invalid input or unsupported configuration exits 2.

Terminal window
restura agent eval ./orders.agent-bundle.json --output agent-report.json

The CLI is intentionally narrower than the desktop AI Lab: it supports stateless OpenAI Responses and Anthropic Messages with environment credentials, deterministic fixtures, and an optional runtime manifest for selected saved HTTP and MCP tools. It refuses desktop secret handles, arbitrary base URLs, judge graders, and sandbox providers. A runtime manifest can only expose explicitly listed read-only HTTP requests and read-only MCP tools with an allowlist; --allow-localhost remains off by default.

name: API smoke tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '24' }
- run: npx restura-cli run ./api-tests --env env.staging.json --reporter live,junit --reporter-output junit=results.xml
- uses: dorny/test-reporter@v1
if: always()
with:
name: API smoke tests
path: results.xml
reporter: java-junit
SymptomLikely cause / fix
No recognised collection layoutTarget needs opencollection.yml / .yaml or legacy _collection.yaml. Re-export from the Restura app.
Invalid URLURL after {{var}} resolution isn’t a valid absolute URL. Check --env is loaded and names match.
Localhost URLs are not allowedAdd --allow-localhost for local upstreams. Off by default to prevent SSRF in shared CI.
gRPC requests return UNKNOWNUpstream doesn’t speak Connect protocol. Restura’s CLI uses Connect-over-HTTP, not gRPC-over-HTTP/2 binary.
a secret handle ref is unresolvable in CLIYour auth uses a desktop-only secret handle. Re-export with inline values for CI use.
Terminal window
# from cli/
npm install
npm test # vitest
npm run type-check # tsc --noEmit
npm run build # tsup → dist/

The CLI imports from the parent project at compile-time via path aliases (@/, @shared/); cli/tsconfig.json controls which parent modules participate in type-checking.