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. Trim it before that.
<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, orscenariofor 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. Needs MCP; offline or on an older n8n it errors naming the capture-based alternative.- 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 — index 0 is the node's main output
[ // the items array for that output
{ "json": { "id": 42, "name": "Ada" } } // each item is { "json": … }
]
] } }
]
}
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.