norn-cli
v4.0.1
Published
Version-controlled API and database tests. Author in VS Code, then run the same files from the CLI and CI.
Maintainers
Readme
Norn
Norn keeps API tests and database checks in repeatable, version-controlled files your whole team can trust. Author, inspect, and debug them in VS Code, then run the same files from the CLI and CI.
Simple API Requests

Chain and Debug API Requests

Why Norn
Most API tools split the work across too many places: one app for sending requests, another for test logic, shell scripts for CI, and a pile of copied values between them. Norn keeps that work in plain text files inside VS Code.
That means you can:
- send single HTTP requests without leaving the editor
- build reusable sequences with variables, captured values, assertions, waits, retries, and branching
- debug those sequences with breakpoints and step-through execution
- run the exact same files from the CLI for smoke tests, regression suites, and pipelines
What You Get
.nornfiles for requests, sequences, and tests.nornenvfiles for environments and secrets.nornapifiles for reusable endpoint definitions.nornsqlfiles for database queries and commands.nornagentfiles for contract-checked AI agents and model-directed delegation- syntax highlighting, IntelliSense, and diagnostics
- response inspection, JSON diffing, and click-to-generate assertions
- tagged and parameterized test execution in VS Code and the CLI
Example
var baseUrl = https://api.example.com
sequence Checkout
POST {{baseUrl}}/auth/login
Content-Type: application/json
{
"username": "demo",
"password": "secret"
}
var token = $1.body.accessToken
GET {{baseUrl}}/orders
Authorization: Bearer {{token}}
assert $2.status == 200
end sequenceThis is the core idea: one file can hold the request, the flow, the captured data, and the assertion. When the flow grows, you still stay in text, version control, and normal code review.
In VS Code
Use Norn to:
- send a request directly from a
.nornfile - run a whole sequence from the editor
- debug a sequence with breakpoints
- run test sequences from the Testing view
In The CLI
The CLI uses the same execution model as the extension, so local runs and CI runs stay aligned.
npm install -g norn-cli
norn ./tests/smoke.norn -e dev.nornenv Templates And Extends
Use [template:name] sections for reusable environment building blocks, then compose selectable [env:name] sections with extends. Only templates can be extended. Templates are not selectable from the VS Code environment picker or CLI; only [env:...] names can be used with -e.
var timeout = 30000
[template:prod]
var baseUrl = https://api.example.com
secret apiKey = prod-key-789
[template:uk]
var dbHost = db.uk.example.com
var bucket = data-uk
[env:prod_uk extends prod, uk]
var failoverHost = api-failover.uk.example.comResolution order is common <- template1 <- template2 <- self, so later templates win on collisions and the env section itself wins over everything. The VS Code editor also shows Activate CodeLens actions, inherited-variable peeks, hover resolution chains, and inlay hints for {{name}} / {{$env.name}} references.
Inlay Hints Across Every File Type
Every {{...}} reference in any Norn file shows its resolved value as a gray inline hint under the active env, with the same masking-for-secrets rules as .nornenv. The three scopes available in .norn resolve in precedence order:
- Sequence-local
vardeclarations inside the currenttest sequence ... end sequenceblock - File-level
vardeclarations at the top of the file (above any sequence) - Active env effective vars (
common← ancestor templates ← self)
{{$env.name}} always reads from scope 3, skipping local/file vars. Runtime values ($1.body.id, request captures, run X() returns) render no hint inline; hover narrates them with source line. .nornapi and .nornsql use env scope only; .nornsql additionally shows the resolved connection string after connection NAME.
Deterministic MCP Tools
Norn can call MCP tools from sequences without leaving the .norn runtime. MCP sessions are deterministic and shared across the full sequence run by default, so nested sequences reuse the same connection for the same resolved server alias.
A server can be declared in two places, and an alias resolves through them in this order:
- an
mcp <Alias> ... end mcpblock in a.nornagentsidecar the file imports — see Contract-Checked Agent Graphs, and prefer this when agents use the server too, because the file then says what it connects to; mcp.serversin the nearestnorn.config.json.
Create a norn.config.json in the root of your project:
{
"version": 1,
"mcp": {
"servers": {
"localTools": {
"transport": "stdio",
"command": ["node", "./tools/mcp-server.js"]
},
"remoteTools": {
"transport": "http",
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer {{$env.mcpToken}}"
},
"timeoutMs": 5000
}
}
}
}Use MCP tools directly inside sequences:
sequence ToolFlow
var tools = run mcp list localTools
var result = run mcp call localTools summarize_text(text: "hello world", format: "short")
assert tools[0].name exists
assert result.structuredContent.summary exists
end sequenceBehavior:
run mcp list <alias>returns the full tool list and drains paginatednextCursorresponses automatically.run mcp call <alias> <tool>(...)supports named arguments or positional arguments bound in tool-schema order, and returns a deterministic result envelope withcontent,structuredContent,isError,text,server, andtool.- Tool
structuredContentis validated against the MCP tool's advertisedoutputSchemawhen present. - A run-scoped session is closed automatically when the outermost sequence finishes or fails. A
sidecar-declared server can narrow that with
session agentorsession call, which close at their own boundary instead. - An imported
.nornhelper sequence resolves aliases through its own imports, not its caller's — the same rule agent scopes follow.
Contract-Checked Agent Graphs
Define agents in an imported .nornagent sidecar. An agent can grant another
agent through agents; the callee then appears to the calling model as an
ordinary tool. Its accepts schema constrains and validates the generated tool
input, and its returns schema validates the result before it travels back up
the graph.
A model block declares the provider, the provider's model, and where the API key
comes from. Nothing is implied by convention: apiKey points at a .nornenv
variable, so the file says exactly which value it needs and the secret itself never
leaves the environment.
model Workbench
provider openai # openai, anthropic, google, or local
name gpt-4o
apiKey {{$env.OPENAI_API_KEY}}
# baseUrl {{$env.OPENAI_BASE_URL}} # optional; required for local
end model
agent DomainExpert
model Workbench
describe "Call for domain questions and include the ticket context."
accepts contracts/domain-question.schema.json
returns contracts/domain-answer.schema.json
system "Answer only the supplied domain question."
end agent
agent TicketRouter
model Workbench
agents DomainExpert
system "Consult the domain expert when the ticket needs it."
end agentGiving agents hands: declared MCP servers
An agent reaches the outside world through MCP servers, and those are declared in the same
file, the same way — every input the server needs written down, credentials as .nornenv
references:
mcp Browser
transport stdio
command npx playwright-mcp --headless --isolated
session agent # run (default) | agent | call
end mcp
mcp House
transport http
url {{$env.HOUSE_MCP_URL}} # the MCP endpoint, path included
header Authorization: Bearer {{$env.HOUSE_TOKEN}}
timeout 60000
end mcp
agent FrontendTester
model Workbench
mcp Browser # every tool this server advertises
tools House.getFixtureUser, House.resetTenant # only these, from this one
system file prompts/frontend-tester.md
end agenttransport stdiospawns the server per run;transport httponly dials one. Norn never starts an http server, so it must already be listening — a refused connection fails before any model spend, naming the URL it tried.mcp <Alias>andtools <Alias>.<tool>are deliberately different statements. A granted tool executes when the model asks for it, so the authored grant is the permission boundary: dropping four characters must not silently widen access from one tool to twenty.headerwrites an HTTP header the way you would write it anywhere else.header Name: value, and the value is the rest of the line taken verbatim — soBearer {{token}}needs no quotes, and quotes you add become part of the token. The colon is optional but recommended: it is what the request side of Norn uses, and IntelliSense inserts it for you.sessiondecides what state is shared. The default,run, gives every agent in a sequence one session — deliberate for an orchestration, and a trap when a backend agent inherits a frontend agent's login and passes a test that should have failed.session agentis how an agent asks for its own.- An alias with no block still resolves through
norn.config.jsonmcp.servers, and a block in the sidecar wins over a config entry of the same name.
Deterministic run mcp steps resolve the same aliases, so a server can be proved reachable
with no model and no cost:
import "./agents.nornagent"
test sequence ServersUp
var tools = run mcp list House
assert tools.length > 0
end sequenceThe runnable demos/mcp-ticket-testing example is the whole
shape end to end: a browser agent, a backend agent narrowed on the same custom server, a
reporter, and a judge over the expected test cases.
A prompt that outgrows its sidecar can live in its own file instead. describe
and system both accept file <path>, resolved relative to the .nornagent
file just like an import:
agent TicketRouter
model Workbench
agents DomainExpert
system file prompts/ticket-router.md
end agentThe file's text is the prompt verbatim — no escaping, so quotes and backslashes
stay as written — and {{...}} references in it resolve exactly as they do
inline. A missing or empty prompt file is a parse error on the directive line.
Run the graph from an ordinary sequence; the same nested contract and retry trace appears in VS Code and the CLI:
import "./agents.nornagent"
test sequence RouteTicket
var ticket = run readJson "./ticket.json"
var verdict = run TicketRouter ticket
assert verdict.text exists
end sequenceMalformed handoffs are returned to the calling model as field-level tool errors so it can correct them. The default guardrails are depth 5, 25 total agent invocations, and a two-failed-attempt contract cap. Configure them globally, per provider, or on an individual agent (most specific wins):
{
"version": 1,
"agents": {
"max_tokens": 16000,
"max_depth": 5,
"max_invocations": 25,
"contract_retries": 2,
"recording": { "enabled": true },
"providers": {
"local": { "max_tokens": 4096 }
}
}
}max_tokens is an output ceiling, not prepaid usage: raising it does not spend
tokens by itself. Depth, invocation, and retry limits can permit additional
model calls, so tighten those three when controlling run cost. See the runnable
demos/agent-workbench example for delegation and a
visible contract correction.
Every sequence run that reaches an agent is recorded under
.norn-cache/runs/ by default, with a rolling cap of 20 files. Recordings hold
the resolved provider request, response, tool transcript, contracts,
conversation state, and ordered hop events. Values declared as secrets in
.nornenv are written as stable named placeholders and restored from the
selected environment when replay starts; a missing value stops replay before a
provider or tool can run. Set agents.recording.enabled to false to disable
automatic recording.
Replay a recording without model calls, MCP calls, or other sequence side effects using the local CLI:
node ./dist/cli.js replay .norn-cache/runs/<recording>.json
node ./dist/cli.js replay .norn-cache/runs/<recording>.json --json
node ./dist/cli.js replay .norn-cache/runs/<recording>.json --from CriteriaComparerPure replay reproduces the stored trace and exits non-zero for failed hops or
contracts, so a deliberately copied recording can be used as a no-key CI
fixture. --from accepts a unique agent name or canonical path such as
TicketRouter[1]/CriteriaComparer[1]; the prefix stays replayed and that hop
onward runs live against the current graph.
Norn: Show Agent Graph, or the CodeLens on any agent block, draws the graph
for a .nornagent file: who calls whom, the accepts/returns contract on each
boundary, and how each one turned out. Hover an agent or a boundary for the full
detail — models, timings, usage, prompts, payloads, contract issues.
The graph's toolbar also lists the project's recorded runs, each labelled with the
.norn file it came from. Pick one to see how it finished, or press Play run
to watch it unfold hop by hop on the canvas. Playback replays the recording's own
events, so nothing re-executes and no model is called; press Stop to jump to
the end. A run from a file the open one took no part in is drawn from that
recording alone — the toolbar says which file it came from, and This file —
current view returns you to the open file, ready for its next live run.
Recording is on by default and keeps the last 20 runs under .norn-cache/runs/.
To step a recording in VS Code, use the existing norn debugger and add the
artifact to a launch configuration:
{
"type": "norn",
"request": "launch",
"name": "Replay RouteTicket",
"file": "${workspaceFolder}/route-ticket.norn",
"sequence": "RouteTicket",
"recording": "${workspaceFolder}/fixtures/route-ticket-run.json",
"stopOnEntry": true
}Agent calls appear as nested frames with Request, Response, Contracts, and Conversation scopes. Normal step controls navigate the recorded hop timeline; Norn Debug: Run to Next Contract Failure stops on retry violations as well as final contract failures. With Agent Graph open, the paused invocation is highlighted and selecting a node moves the replay cursor to that invocation.
Good Fit For
- backend teams validating APIs during development
- QA and automation work that needs readable test flows
- regression and smoke suites that should run the same way locally and in CI
- projects that want API requests and API tests to live next to the code
Diff preview test line.
ping
