node run
n8n-decanter node run <node-file> [fixture.json] [--allow-env]
Executes a node’s body against an emulated n8n context ($input, $json,
$('Node'), $jmespath, DateTime, $getWorkflowStaticData, …) and prints the
items it returns. Fully offline — no credentials, no network. Prefer this over
hand-rolling a throwaway test script.
run is the fast, offline approximation rung of the verification ladder — it
is not a faithful n8n runtime. Where a global’s value genuinely lives on the
instance, run says so and points you at test (which runs
the real n8n draft over MCP) instead of guessing. See
the boundary below.
The run mode (runOnceForAllItems / runOnceForEachItem) is read from the
node’s entry in workflow.json, so each-item nodes are looped once per input
item.
What’s emulated vs. unsupported
| Global | Status | How run handles it |
|---|---|---|
$input, $json, $binary |
✅ Covered | from the fixture input (defaults to one empty item); an array of arrays pins one per input index |
$('Node'), $node, $items() |
✅ Covered | views over the fixture nodes map, per-output entries included — see the branch-index note below |
$jmespath / $jmesPath |
✅ Covered | real JMESPath — jmespath@0.16.0, the version n8n pins |
DateTime / Duration / Interval |
✅ Covered | Luxon, exactly as in n8n |
$now / $today |
✅ Covered | Luxon DateTime — now / start-of-day |
console |
✅ Covered | prints to your terminal (n8n shows it in the execution log) |
$getWorkflowStaticData |
✅ Covered | seeded from workflow.json’s staticData / the fixture |
$env |
✅ Pinnable | fixture env, or --allow-env to inherit the process env |
$workflow, $execution, $prevNode |
🟡 Stub / pinnable | a small stub, or the fixture value |
$nodeId / $nodeVersion / $webhookId |
🟡 From the node | read from workflow.json’s node entry (stubbed if none) |
$runIndex / $itemIndex |
🟡 Partial | pinned at 0 (the each-item loop advances $itemIndex) |
$('Node').item / .itemMatching() |
🟡 Partial | approximate — reads the fixture by position, not true paired-item linking |
$vars / $secrets |
🟡 Pin or escalate | pin in the fixture, else a friendly signpost to test |
$evaluateExpression |
⛔ Unsupported | needs n8n’s expression engine → signposts test |
$if / $min / $max / $ifEmpty |
⛔ Not a Code-node global | n8n expression-language helpers ({{ }} only) — they throw in real n8n’s Code node too, so they’re not provided |
Branch indexes are answered when pinned, refused when not — never guessed.
$('Node').all(1), $items('Node', 1) and $input.all(1) ask for a node’s
second output — an IF’s false branch, an error output. Pin it by giving that
node one items array per output:
"nodes": { "Decide": [[{ "json": { "side": "true" } }], [{ "json": { "side": "false" } }]] }
all(1), first(1) and last(1) then read output 1, and an empty array is
a real answer (that branch took no items). Ask for an output the fixture does
not supply and run says so — naming the call and how many outputs it has —
rather than handing back output 0’s items, which is wrong data that looks right.
Same escape hatch as always: run it for real with test.
When emulation isn’t enough, escalate to test. A node that needs a real
$vars/$secrets value, true paired-item linking, real execution ids, or
$evaluateExpression should run against the instance with
test — or pin the value it needs in the fixture. run
refuses an instance-scoped global with a message that names the global and
points here, never a bare ReferenceError.
Fixtures
The optional fixture JSON supplies the context; every field is optional:
{
"input": [{ "json": { "sku": "A1" } }],
"nodes": { "Fetch Products": [{ "json": { "id": 1 } }] },
"params": { "keepOnlySet": true },
"env": { "REGION": "eu" },
"vars": { "apiBase": "https://api.example.com" },
"secrets": { "vault": { "token": "s3cr3t" } },
"staticData": { "global": { "cursor": 42 } },
"workflow": { "id": "42", "name": "Order Sync", "active": true },
"execution": { "id": "1001", "mode": "manual" },
"prevNode": { "name": "Fetch Products", "outputIndex": 0, "runIndex": 0 }
}
inputfeeds$input/$json; without a fixture the input defaults to a single empty item. An array of arrays is one items array per input index, so$input.all(1)reads a Merge node’s second input.nodesbacks$('Node Name'),$node['Node Name'], and$items('Node Name'). An array of arrays is one items array per output (see branch indexes above); a plain items array is a single output, as before. That means a single output whose items are themselves arrays needs the explicit item form ([{ "json": [1, 2] }]).paramsbacks$input.params(defaults to{}).envbacks$env. Like n8n’s own scoped$env, it is empty by default — set it explicitly with this field, or pass--allow-envto inherit the CLI process’s environment (which may includeN8N_API_KEYand other secrets), so a node that prints$envnever leaks the host environment by accident.vars/secretsback$vars/$secrets. These are instance-scoped — without a fixture valueruncan’t know them, so any access throws the friendly “not emulated inrun— usetest, or pinvars/secrets” message. Pin them here to run a node that reads them offline.$getWorkflowStaticDatais seeded fromworkflow.json’sstaticData(globaland this node’s slice); a fixturestaticDatareplaces the matching slice ("node"refers to the node being run). Mutations are visible during the run but never persisted —runis offline.workflow,execution, andprevNodeback$workflow,$execution, and$prevNode; each defaults to a small stub ($workflow→{ id: "local", name: "local", active: false },$execution→{ id: "local", mode: "test" }) when omitted.
For real input shapes instead of hand-written ones, fetch production run data with executions and copy a node’s items into your fixture.