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) |
$('Node'), $node, $items() | ✅ Covered | views over the fixture nodes map — but 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 refused, not guessed. $('Node').all(1),
$items('Node', 1) and $input.all(1) ask for a node’s second output — an
IF’s false branch, a Switch’s other case. A fixture pins one items array
per node, so there is no honest answer, and run says so instead of handing back
output 0’s items (which is what it used to do — wrong data that looks right).
Pin that branch’s items as their own fixture node, or 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.nodesbacks$('Node Name'),$node['Node Name'], and$items('Node Name').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.