scenario
n8n-decanter scenario create <workflow> ["<slug>"] [--execution <id>] [--scaffold] [--json]
n8n-decanter scenario create <workflow> "<slug>" --extend
n8n-decanter scenario check <workflow> ["<slug>"] [--json]
Both take a workflow ref first. Leaving it off on a terminal opens the picker to choose one (same as every other ref-taking verb); piped/non-TTY runs still error with the usage line, so scripts and agents never block.
A scenario is a named, committed input set for your workflow — captured
from a real run or scaffolded from its schemas — that the two replays,
test (on your instance) and
preflight --simulate (on a local
engine), run and diff against. It’s the only committed pin artifact:
workflows/<folder>/scenarios/<slug>.json
is a self-contained, execution-shaped file, so preflight --simulate --scenario <slug> /
test --scenario <slug> replay it directly, no precedence rules to reason
about. Everything here is offline — no engine for scenario create/check
themselves, and no LLM API or key: you (or your IDE agent) author the
values.
A gap is a network node reached during a replay with no pinned data — a node added or reparametrized since the capture, or every node when building a scenario from scratch. Both replays hard-error on a gap; a scenario is how you supply the missing data as a reproducible, reviewable set.
The two replays do not demand the same nodes
This matters when you fill a scenario by hand, so it is stated plainly:
| demands pin data for | |
|---|---|
preflight --simulate |
network nodes the capture actually reached. An unreached node is neutralized (it throws if something reaches it), so an untaken branch is exempt. |
test |
every enabled, non-pure, non-loop-driver node — reachable or not. |
test is stricter on purpose: it runs on the live instance with real
credentials, so a node reached unexpectedly there would hit the real world,
while a --simulate node reached unexpectedly only stops the replay.
You do not normally have to care, because scenario create --execution closes
the difference for you: a pinnable node the capture never reached is pinned to an
empty run and listed under _decanterScenario.notExercised. That is an
honest claim (“this branch isn’t exercised”), not invented output, and it keeps
the node from running for real. Review it — if a branch should have run,
give it real data instead.
For a scenario written before this, or one you hand-edited, scenario check
tells you where you stand and --extend fills the difference:
n8n-decanter scenario check order-sync happy-path
# ✓ scenario "happy-path": valid
# ! complete for `preflight --simulate`, but `test` needs 2 more nodes: Notify, Archive
# → n8n-decanter scenario create order-sync "happy-path" --extend
scenario create
Two seeds, composable:
n8n-decanter scenario create order-sync "happy-path" --execution 4812
# copies executions/4812.json -> scenarios/happy-path.json, flags the gap nodes
n8n-decanter scenario create order-sync "happy-path" --execution 4812 --scaffold
# same, plus annotates each gap with its output JSON Schema
n8n-decanter scenario create order-sync "from-scratch" --scaffold
# no capture: every pinnable node becomes a schema-annotated fill entry
n8n-decanter scenario create order-sync "happy-path" --extend
# existing scenario: add the pinnable nodes it is missing, keep every value
Watch the size. A capture-seeded scenario is a verbatim copy of every
item of every node — one from a busy production run can be tens of megabytes.
scenario create prints the size, and warns above 1 MB, because scenarios/ is
tracked: the folder-wide auto-commit that pull and push perform sweeps it
into git history on the next sync, permanently.
scenario create itself never commits, so an oversized scenario is still
only a file on disk when you are warned. Two ways to act on that, both
immediate:
- Cut it down by hand. Delete items from
data.resultData.runDatain your editor. The file is written indented for exactly this — keep at least one item per output a node actually emitted, and do not remove whole runs (a loop capture’s run counts are what mark it as a loop). - Do not seed from the capture at all. Delete the file and re-create it with
--scaffoldinstead of--execution. A scaffold carries no capture data — you author the pins yourself, guided by thefilllist.
There is no --trim flag, deliberately: a trimmed capture is no longer a
faithful record of a real run, so it could not serve as the diff baseline that
makes a scenario worth tracking.
-
<slug>names the scenario (happy-path,empty-cart,error-case) and becomes the filename (kebab-cased). Optional — omit it and the scenario is named after the execution id (scenarios/4812.json, orscaffoldfor a slug-less pure scaffold). Keep a library of scenarios per workflow. -
--execution <id>seeds the scenario from a captured execution (executions/<id>.json); nodes with captured output are recorded ascaptureprovenance, each remaining gap is listed under_decanterScenario.fill. -
--scaffoldcalls n8n’s read-only MCP toolprepare_test_pin_dataand annotates each gap with its output JSON Schema (expectedSchema), provenancescaffolded. It never invents values — the tool returns schemas and coverage counts only, no data (readOnlyHint: true); a person or agent still authors every value, reviewed in the diff like any other scenario edit. Composes with--execution: the capture seeds what it covers,--scaffoldannotates the remaining gaps. A bare--scaffoldwith no--executionbuilds a from-scratch set where every pinnable node is a fill entry.It works with no instance. The fill entries come from your local
workflow.json; the schemas are an annotation on top. With noN8N_HOSTconfigured,--scaffoldsays so and scaffolds anyway — every node lands as provenanceauthoredinstead ofscaffolded, and_decanterScenario.sourcestill readsscaffold. That is the difference between a less-annotated scenario and no scenario at all, and it is what makespreflight --offline --simulatereachable from a train. -
Neither
--executionnor--scaffoldgiven → defaults to the newest capture underexecutions/(same as the replays’ default). -
--jsonprints{ slug, file, gaps, coverage }for tooling (coverageonly present when--scaffoldran). -
A capture-seeded scenario copies real captured data (which can hold credentials/PII) —
scenario createprints a review warning; check before committing. It refuses to overwrite an existing scenario, so it never clobbers data you’ve filled in.
The written file is a verbatim copy of the capture (or a bare skeleton for a
pure scaffold) plus a _decanterScenario block listing each gap node with its
type, parameters, an inputSample, and — when scaffolded — its
expectedSchema:
{
"id": 4812,
"data": { "resultData": { "runData": {
"Trigger": [ /* real captured runs, untouched */ ],
"Compute": [ /* … */ ]
// add "Enrich Customer" here ↓
} } },
"_decanterScenario": {
"source": "capture+scaffold",
"sourceExecution": "4812",
"createdAt": "2026-07-21",
"workflowVersionId": "…",
"guidance": "For each node in \"fill\", add data.resultData.runData[\"<node>\"] = [ { \"data\": { \"main\": [ [ { \"json\": { …output… } } ] ] } } ], using its type/parameters/inputSample/expectedSchema as context. Keep \"fill\" as-is — scenario check validates it. Then: preflight --simulate --scenario happy-path.",
"fill": [
{
"node": "Enrich Customer",
"type": "n8n-nodes-base.httpRequest",
"parameters": { "url": "https://api.crm.internal/customers/{{$json.id}}" },
"inputSample": [ { "id": 42, "email": "a@b.com" } ],
"expectedSchema": { "type": "object", "properties": { "id": { "type": "number" }, "name": { "type": "string" } } }
}
]
}
}
You (or your agent) add each fill node’s runData using its context (type,
parameters, inputSample, and expectedSchema when present), and leave
fill in place — it records which nodes are synthetic (the provenance
signal) and is what scenario check validates.
scenario check
Structurally validates a scenario offline — the fast loop while filling, no Docker needed:
n8n-decanter scenario check order-sync happy-path # one scenario
n8n-decanter scenario check order-sync # every scenario in the folder
Exits 1 if any scenario is malformed or has a fill node still empty, with a
node-named error:
scenario scenarios/happy-path.json is invalid:
- Enrich Customer run 0 item 0: each item needs a "json" field
- incomplete: add runData for Enrich Customer (still listed in _decanterScenario.fill)
expected per node: runData["<node>"] = [ { "data": { "main": [ [ { "json": { … } } ] ] } } ]
n8n publishes no JSON Schema for execution data — the format lives only in
the n8n-workflow TypeScript types (IRunExecutionData → ITaskData →
INodeExecutionData). scenario check is decanter’s own structural check of
the exact shape it replays. preflight --simulate --scenario and
test --scenario run the same check when they load a scenario, so a bad file
never reaches the engine or the instance.
The shape to match, per node:
"runData": {
"Enrich Customer": [ // one entry per run (a normal node runs once)
{ "data": { "main": [ // outputs — ONLY index 0 is ever replayed
[ // the items array for that output
{ "json": { "id": 42, "name": "Ada" } } // each item is { "json": … }
]
] } }
]
}
Several outputs: the sim replays them, test cannot
A node can emit on more than one output — an error output, an IF’s two
branches — and your scenario may carry items on each of them. The two replay
paths differ there, and scenario check tells you which one you are looking at:
preflight --simulatereplays every populated output. Decanter owns that workflow copy, so each extra output gets its own stand-in node fed by the same input as the original — the error branch really runs.testreplaysmain[0]only. It hands n8n apinDatamap, and that format is one flat items array per node: there is no output dimension to hand it. So on the instance, whatever the further outputs feed receives nothing and emits nothing — and the run still finishes “successfully”.
scenario check says so rather than letting you find out from an empty run. It
warns when
- a node in the scenario has items on more than one output, naming the
indices
testwill drop, and - a node source reads a pinned node’s non-first output —
$('Enrich Customer').all(1)or$items('Enrich Customer', 1)— the call that quietly returns nothing on the instance. The warning also says whether the scenario covers that output, i.e. whetherpreflight --simulatecan answer it.
Both are warnings, not errors. The way out is preflight --simulate for a
branch the scenario covers, pinning that output as its own node, or running the
workflow live with test instead of replaying it. The
test coverage line is the
same problem caught one step later.
The full loop
n8n-decanter preflight order-sync --simulate --execution 4812 # ✗ gap: Enrich Customer has no data
n8n-decanter scenario create order-sync "happy-path" --execution 4812
# → fill scenarios/happy-path.json's runData for the flagged nodes
n8n-decanter scenario check order-sync happy-path # ✓ valid (offline, fast)
n8n-decanter preflight order-sync --simulate --scenario happy-path # replay the scenario
Provenance and synthetic pins
Each node’s pins in a scenario carry a provenance: capture (real
execution data — can serve as the diff baseline), authored
(hand/agent-filled with no schema), or scaffolded (schema-guided fill,
--scaffold’s expectedSchema). A scenario with any non-capture node is
synthetic pins — both replays label the run “synthetic pins —
proves executability, not output correctness”: no per-node diff is
asserted, and divergence is informational, not a fail. A capture-only
scenario (no fill entries left) keeps the full per-node diff and
exit-1-on-divergence semantics unchanged. --json reports gain
syntheticPins: boolean and provenance: Record<node, "capture"|"authored"|"scaffolded">.
Committed and reproducible
Unlike executions/ (gitignored temp data), scenarios/ is tracked in
git, so a scenario-based replay is reproducible for teammates and CI.
Scenarios are chosen explicitly by slug (preflight --simulate --scenario <slug> /
test --scenario <slug>) — they’re named scenarios, not a “latest” default.
Migration and removed mechanisms
- A legacy
mocks/dir (the pre-rename name) auto-migrates toscenarios/the first time any verb touches it — a plain git-recorded rename, so history follows. It refuses when both the legacymocks/andscenarios/exist (merge them by hand first). The legacy metadata key_decanterMockis still read (as_decanterScenario) for files written before the rename. - The legacy per-node
fixtures/<node>.jsonmechanism and the old--pinflag are removed outright — no read path. A leftover legacyfixtures/dir is a hard error from the compliance guard (so frompush,watch, andpreflightalike), naming the replacement: recreate the data as a scenario (scenario create --execution <id>), then delete the legacyfixtures/dir.
Relation to the official n8n skills
n8n’s own n8n-workflow-lifecycle-official skill teaches agents an
ephemeral in-session pin flow: prepare_test_pin_data → the agent
generates values → test_workflow, per-execution, nothing persisted.
Scenarios are decanter’s durable counterpart to the same tool pair: a
scenario is committed, human-reviewed, reused across runs, and (when
capture-seeded) diffed against real data — composing with the official flow
rather than competing with it.