Menu

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 --simulatenetwork nodes the capture actually reached. An unreached node is neutralized (it throws if something reaches it), so an untaken branch is exempt.
testevery 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, or scenario for 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 as capture provenance, each remaining gap is listed under _decanterScenario.fill.
  • --scaffold calls n8n’s read-only MCP tool prepare_test_pin_data and annotates each gap with its output JSON Schema (expectedSchema), provenance scaffolded. 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, --scaffold annotates the remaining gaps. A bare --scaffold with no --execution builds 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 --execution nor --scaffold given → defaults to the newest capture under executions/ (same as the replays’ default).
  • --json prints { slug, file, gaps, coverage } for tooling (coverage only present when --scaffold ran).
  • A capture-seeded scenario copies real captured data (which can hold credentials/PII) — scenario create prints 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 (IRunExecutionDataITaskDataINodeExecutionData). 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 to scenarios/ the first time any verb touches it — a plain git-recorded rename, so history follows. It refuses when both the legacy mocks/ and scenarios/ exist (merge them by hand first). The legacy metadata key _decanterMock is still read (as _decanterScenario) for files written before the rename.
  • The legacy per-node fixtures/<node>.json mechanism and the old --pin flag are removed outright — no read path. A leftover legacy fixtures/ dir is a hard error from the compliance guard (so from push, watch, and preflight alike), naming the replacement: recreate the data as a scenario (scenario create --execution <id>), then delete the legacy fixtures/ 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.