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
.norn files for requests, sequences, and tests
.nornenv files for environments and secrets
.nornapi files for reusable endpoint definitions
.nornsql files for database queries and commands
.nornai or .nornagent files 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 sequence
This 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
.norn file
- 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
Update an existing global installation from npm with:
norn update
For a hosted agent workflow, select one sequence and supply its runtime inputs explicitly:
norn run ./workflows/ticket.norn \
--sequence HandleTicket \
--param NORN-482 \
--param-json context=./inputs/ticket-context.json \
--recording ./artifacts/ticket-run.json \
--json
--param [name=]value is repeatable and preserves the value as a string.
--param-json [name=]file.json is also repeatable; it parses the file and assigns its typed JSON
value directly to the declared sequence parameter. Unnamed values follow declaration order and
skip parameters supplied by name, so named and positional inputs can be mixed. CLI values override
authored defaults. Both forms require one file plus --sequence, reject undeclared or missing
required parameters before execution, and cannot be combined with @data/@theory cases.
--recording writes the normal secret-masked agent recording to an exact artifact path instead of
the rolling local cache.
Ordinary sequence runs handle SIGINT and SIGTERM as graceful cancellation requests. A repeated
signal forces exit; cancelled processes use status 130 (SIGINT) or 143 (SIGTERM).
.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.com
Resolution 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
var declarations inside the current test sequence ... end sequence block
- File-level
var declarations 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.
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 mcp block in a .nornai sidecar 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.servers in the nearest norn.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 sequence
Behavior:
run mcp list <alias> returns the full tool list and drains paginated nextCursor responses automatically.
run mcp call <alias> <tool>(...) supports named arguments or positional arguments bound in tool-schema order, and returns a deterministic result envelope with content, structuredContent, isError, text, server, and tool.
- Tool
structuredContent is validated against the MCP tool's advertised outputSchema when present.
- A run-scoped session is closed automatically when the outermost sequence finishes or fails. A
sidecar-declared server can narrow that with
session agent or session call, which close at
their own boundary instead.
- An imported
.norn helper 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 .nornai or .nornagent sidecar. Both extensions use the same syntax and behavior. 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 agent
Giving 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 agent
transport stdio spawns the server per run; transport http only 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> and tools <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.
header writes 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 — so Bearer {{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.
session decides 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 agent
is how an agent asks for its own.
- An alias with no block still resolves through
norn.config.json mcp.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.nornai"
test sequence ServersUp
var tools = run mcp list House
assert tools.length > 0
end sequence
The 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 agent sidecar
file just like an import:
agent TicketRouter
model Workbench
agents DomainExpert
system file prompts/ticket-router.md
end agent
The 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.nornai"
test sequence RouteTicket
var ticket = run readJson "./ticket.json"
var verdict = run TicketRouter ticket
assert verdict.text exists
end sequence
Malformed 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 CriteriaComparer
Pure 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 an agent sidecar: 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