# n8n-decanter > The toolkit for building code-heavy n8n workflows — agent-first, MCP-native. Every Code node's source becomes its own .js/.ts file in git: typed TypeScript with shared libraries, verified by preflights (offline check/simulate, instance-side test, or the preflight gate that scores the whole ladder), and synced draft-first over n8n's built-in MCP server. It also guards your agent's access to n8n's full MCP surface — create, read, update, and rename whole workflows through it, with only Code-node writes redirected to files — and mirrors each workflow's structure into a read-only snapshot. --- # Installation Source: https://buttjer.github.io/n8n-decanter/docs/getting-started/installation/ Requires **Node >= 22.18** — the CLI is TypeScript (`.mts`), executed natively via Node's type stripping; there is no build step for development. ```sh npm install -g n8n-decanter ``` Alternatives: - **From a git checkout:** `npm link` (run `npm run build` once first — the installed bin is the compiled `dist/`), or invoke `node n8n-decanter.mts …` directly, no build needed. - **Per sync dir:** add `n8n-decanter` to the sync dir's `devDependencies` instead of installing globally. A local install lands in `node_modules/.bin`, **not** on your `PATH`, so invoke it as **`npx n8n-decanter `** (or via an `npm run` script — npm puts `node_modules/.bin` on `PATH` itself). A bare `n8n-decanter ` is the one form that won't resolve. `npx n8n-decanter …` works under a global install too, so it is the safe form to use in scripts and docs shared across a team. The agent config `init` scaffolds allows **both** forms, and the MCP guard it wires up uses `npx --no-install` for exactly this reason. ## Old Node fails with a `SyntaxError` On Node older than 22.18 the CLI fails at startup with a confusing `SyntaxError` rather than a clean version message: npm's `engines` field only *warns* at install time (unless you set `engine-strict`). If you see a syntax error pointing into a `.mts` file, check `node --version` first. Next: [Quickstart](/docs/getting-started/quickstart/) — set up a sync dir and pull your first workflow. --- # Quickstart Source: https://buttjer.github.io/n8n-decanter/docs/getting-started/quickstart/ ## 1. Bootstrap a sync dir ```sh n8n-decanter init [dir] ``` `init` prompts for your n8n host, connects via **OAuth** in your browser (or takes a pasted MCP token), offers the optional public API key, copies the starter template, and scaffolds config, `.gitignore`, TypeScript tooling, and agent configs. Re-running is safe: files you've edited are left alone (untouched template files can be refreshed after a confirm; `--force` resets everything), and it does a best-effort connection check. See [init](/docs/cli/init/) for details. One-time n8n-side setup: enable **MCP access** (n8n → Settings → MCP; needs an n8n with the built-in MCP server, ~2.20+), and flip **"Available in MCP"** on each workflow you want to sync (workflow card ⋯ menu, or workflow settings). ## 2. Pull a workflow Workflows are born in n8n — create one there (even empty) and switch on **"Available in MCP"** (step 1); only opted-in workflows can be pulled. Then, on a terminal, run `pull` and pick one from the list: ```sh n8n-decanter pull # picker: lists your n8n workflows → pick one to pull n8n-decanter pull # …or pull directly (scriptable, no TTY needed) ``` With no argument on a terminal, `pull` lists your n8n workflows (remote ones too) so you can pick one — no config entry or id needed. Each workflow lands as a folder under `workflows/`: a read-only `workflow.json` structure snapshot plus one source file per Code node in a `code/` subdir — see [Sync layout](/docs/concepts/sync-layout/). After every successful pull and push, the workflow's folder is git-committed automatically (scoped to that folder; outside a git repo it just warns). To fix a default set that a bare `pull`/`push`/`diff`/`preflight` acts on, list ids in `decanter.config.json` (`"workflows": ["0cXNQKKzmO0pXiCq"]`); all keys are documented in [Configuration](/docs/concepts/configuration/). ## 3. Edit and push Edit the node files in your IDE (or let your agent do it), then verify with [preflight](/docs/cli/preflight/) — the single gate: layout, types, and, unless you pass `--offline`, read-only drift checks against the instance. [node run](/docs/cli/node-run/) executes one node locally when you want to see its actual output. ```sh n8n-decanter preflight # the gate (add --offline for static-only, no network) n8n-decanter push # lands on the workflow's DRAFT n8n-decanter publish # take it live (or: push --publish) ``` Every push updates the workflow's **draft** — the live version keeps running until you `publish`. Push refuses to overwrite remote code changes made since the last sync and blocks on layout or type errors — the [push gates](/docs/concepts/push-gates/) page explains the guard rules. To read the actual changed lines first, [diff](/docs/cli/diff/) prints a per-node unified diff of your files against the draft. For a save-to-push loop, see [watch](/docs/cli/watch/) — the open n8n editor updates live on each push. --- # Overview Source: https://buttjer.github.io/n8n-decanter/docs/cli/overview/ **The verb comes first:** `n8n-decanter [workflow…] [flags]`. Everything after the verb is an argument, so a workflow named like a verb is just a normal argument (no special rule). Flags may still sit anywhere. ```sh n8n-decanter # interactive picker (terminal, inited project) n8n-decanter --version # print the installed version and exit (-v; errors if combined with a verb) n8n-decanter help # the command surface (also --help, or a bare run when piped) # Setup n8n-decanter init [dir] [--force] # bootstrap (add --host/--token/--api-key to skip prompts) n8n-decanter completion zsh|bash # Sync — over n8n's MCP server, Code-node source only (structure lives in n8n) n8n-decanter pull [workflow…] # code + structure snapshot -> workflows// n8n-decanter push [workflow…] [--force] [--publish] [--no-typecheck] # to the DRAFT n8n-decanter watch [workflow] n8n-decanter publish [workflow…] # take the draft(s) live n8n-decanter unpublish [workflow…] # back to draft-only # Inspect & test n8n-decanter preflight [workflow…] [--simulate] [--offline] [--viewer] [--json] [--fail-on=warn] [--fail-fast] [--require=] # the gate: grades LOCAL code, scored (read-only) — then push, then test # --simulate ADDS a local-engine run (Docker); --offline DROPS the instance reads n8n-decanter diff [workflow…] # per-node line diff, local code vs the n8n draft (always exits 0) n8n-decanter executions [workflow…] [--status=…] [--limit=N] n8n-decanter executions [workflow…] clean n8n-decanter data-tables [table…] [--filter=''] [--search=…] [--sort=col:asc|desc] [--limit=N] [--all] n8n-decanter data-tables [table…] clean n8n-decanter test [--execution | --scenario ] [--trigger ] [--json] # grades the INSTANCE's draft. bare = static check, nothing runs; # --execution/--scenario = pinned run on the instance n8n-decanter scenario create [""] [--execution ] [--scaffold] # committed, gap-fillable pin-data set (offline; --scaffold needs MCP) n8n-decanter scenario check [""] # structurally validate a scenario (offline) # Backup — git-native, redeployable disaster recovery (REST; needs N8N_API_KEY) n8n-decanter backup create # capture a full-export backup into backups/ n8n-decanter backup restore [] # redeploy as a NEW, unpublished workflow n8n-decanter backup list # retained backups (offline) n8n-decanter list [--remote] [--json] # Node n8n-decanter node run [fixture.json] [--allow-env] # run a node locally (offline) # Agent guard — structure/lifecycle acts go through n8n's MCP, guarded n8n-decanter mcp connect # stdio MCP guard (spawned from the scaffolded .mcp.json; no secret) n8n-decanter mcp serve [--port N] # HTTP variant: localhost guard-proxy for URL-configured agents ``` Creating, renaming, and archiving workflows — and adding or renaming nodes — are **n8n's acts**: do them in the n8n editor or over n8n's MCP tools (your agent reaches them through the [guard](/docs/cli/mcp-connect/), which blocks only Code-node `jsCode` writes). The next [pull](/docs/cli/pull/) reconciles the local mirror: files follow renames, new Code nodes land as files, and the first push seeds a node born empty. ## Placeholder vocabulary | Token | Means | | --- | --- | | `` / `[workflow…]` | a workflow: **id · name · unique name-prefix · folder name** | | `` | a path to a node source file (`node run`) | | `` | an n8n execution id (numeric) — `preflight --execution`, `test --execution`, `executions ` | | `` | a scenario name — `scenario create`/`scenario check`, `preflight --scenario`/`test --scenario` (kebab-cased) | | `` | a comma list of [preflight](/docs/cli/preflight/) check ids — `preflight --require=layout,simulate` | | `` | a backup: **timestamp (or a prefix, e.g. a bare date) · versionId (short or full)** — `backup restore` | ## Interactive picker Running **bare `n8n-decanter`** (no verb, no arguments) in an inited project on a terminal opens a picker instead of printing usage: type to filter, `↑`/`↓` to move. Each row leads with a status glyph — `●` for a pulled workflow (green), `○` for a not-yet-pulled remote one (yellow), `⊘` for a remote workflow **not yet available in MCP** (red, sorted last) — so the state reads by shape, not color alone, and the ids line up in an aligned column. `Enter` on a pulled workflow offers `preflight` / `preflight --simulate` / `diff` / `pull` / `push` / `watch` / `executions` — a row may carry flags, and the `--simulate` row runs the browsable [`--viewer`](/docs/cli/preflight/#--viewer--browse-the-run-in-a-real-n8n) form. `Enter` on an unpulled one pulls it directly; `Enter` on a `⊘` row explains where to flip the "Available in MCP" switch in n8n. It stays in the workflow's verb menu between runs, `Esc` backs out to the list, `Esc` again quits. Piped output and dirs without a `decanter.config.json` keep printing usage — scripts and LLM harnesses never see the picker. **Pulled workflows are listed newest-synced first** — the one you last pulled or pushed is under the cursor when the picker opens, so the workflow you are actually working on doesn't have to be hunted for. Unpulled remote rows keep their place after the local ones. The order comes from each workflow folder's sync timestamp, which is *local activity* and not committed history: right after a fresh `git clone` everything looks equally recent, so the list falls back to alphabetical until your first pull or push. The scripted [`list`](/docs/cli/list/) output is unaffected — it stays alphabetical. **A drift failure offers a `--force` retry.** If a `push` from the picker aborts because the code changed in n8n since your last sync, the picker asks `retry with --force and overwrite the remote draft? [y/N]` instead of just printing the hint and dropping back to the menu. The default is **No** — a bare `Enter` (or anything other than `y`/`yes`) declines and returns to the menu, and answering `y` re-runs the same action with `--force`, which overwrites the n8n **draft** only. The offer appears *only* for failures `--force` can actually fix: a [layout-compliance](/docs/cli/preflight/) error never prompts, because forcing would not help. Non-interactive runs are unchanged — they never prompt, they print the `--force` hint and exit non-zero. **No-ref → picker.** A ref-taking verb given *no* workflow, on a terminal, opens the picker to choose one and then runs that verb on it (the verb menu is skipped). The same newest-synced-first ordering applies. For `pull` the list includes **remote** workflows too (as in the bare picker), so a fresh setup with nothing pulled still gets a menu to pick from; the other verbs act on already-pulled workflows only. This includes the `backup …` and `scenario …` sub-verbs, whose first argument is a workflow ref. Piped/non-TTY runs keep the config-default / error path unchanged, so scripts and LLM harnesses never block. The force-retry confirm belongs to the interactive picker *session* (bare `n8n-decanter`), so this single-select path prints the ordinary `--force` hint instead. ## Workflow refs A `` is its **id, its workflow/folder name, or a unique name prefix** — `n8n-decanter push "Order Sync"` and `n8n-decanter push order` both work. Matching is case-insensitive and never prompts: an ambiguous or unknown name errors with the candidate list. `pull` resolves not-yet-pulled names against the server's workflow list. Without a workflow argument, all workflows from the config are processed (or the picker opens, on a terminal). **Verb-first grammar.** The verb is the first argument; everything after it is an argument. `n8n-decanter diff push` runs `diff` on the workflow named `push` — no "address it by id" caveat. Verb-last (`n8n-decanter wf123 push`) errors with *unknown verb*. Flags may still appear in any position. ## Offline vs. online | Verbs | Network | | --- | --- | | `preflight --offline`, `node run`, `list`, `scenario check`, `completion`, `executions clean`, `data-tables clean` | Fully offline — no credentials needed (`list --remote` is the exception; `preflight --offline --simulate` needs Docker but never the n8n instance; `scenario create --scaffold` is the exception in the `scenario` namespace — it needs MCP) | | `diff`, `list --remote`, `executions`, `data-tables`, `backup create`/`restore` | Read the remote (`backup restore` also writes a **new** workflow, never touching the source) | | `backup list` | Fully offline — reads the local `backups/` store | | `test` | Grades the workflow's **draft** on the instance — run it **after a push** so the draft holds your code. **Bare**: a static check (dangling `$('…')` references), nothing executes, no capture needed. **With `--execution`/`--scenario`**: a pinned run. There is no fallback to the newest capture — executing means saying so. On a terminal, when local differs: a **published** workflow gets a local-vs-draft prompt; an **unpublished** one is pushed without asking (a draft nobody runs). Non-interactive runs never write | | `preflight` | Verifies your **local** code as one scored gate — static + instance reads, plus an optional local-engine replay (`--simulate`); **never writes and never runs on the instance**, with any flag combination. `--offline` drops the instance reads entirely. Run it *before* `push`; `test` comes after | | `pull`, `push`, `watch`, `publish`, `unpublish` | Read/write the live instance (pushes land on the **draft**) | | `mcp connect` / `mcp serve` | Long-running MCP guard (stdio / localhost HTTP) — forwards an agent's MCP traffic to the instance with decanter's credentials, blocking Code-node (`jsCode`) writes; a forwarded structure edit also triggers a background `workflow.json` refresh (`liveMirror`, on by default) | Credentials come from `.env` next to `decanter.config.json` (searched upward from the current directory) or the environment. `N8N_HOST` plus **MCP credentials** (OAuth minted by [`init`](/docs/cli/init/) into `.decanter-auth.json`, or an `N8N_MCP_TOKEN`) power the sync and lifecycle verbs; the **public API key** (`N8N_API_KEY`, optional) powers only `executions`, `data-tables`, and `backup` — the surfaces n8n's MCP server doesn't cover. ## Output and scripting Output is styled (color, `✓`/`!`/`✗` glyphs, progress) **only when writing to a terminal** and respects `NO_COLOR`/`FORCE_COLOR`; piped or redirected output is plain line-oriented text, safe for scripts and LLM harnesses. API requests time out after 30 s (set `"requestTimeoutMs"` in `decanter.config.json` for slow instances). `DEBUG=1` prints full stack traces on errors. Tab completion for verbs, flags, and workflow names: ```sh eval "$(n8n-decanter completion zsh)" # or bash — append to your shell rc ``` --- # init Source: https://buttjer.github.io/n8n-decanter/docs/cli/init/ ```sh n8n-decanter init [dir] [--force] n8n-decanter init [dir] --host [--token ] [--api-key ] # non-interactive ``` Interactive setup for a new (or existing) sync dir: - Prompts for the n8n host. If you leave off the scheme, local addresses (`localhost`, loopback, private LAN ranges, `*.local`) default to `http://` and everything else to `https://` — type the scheme yourself to override. Then connects via **OAuth**: your browser opens n8n's consent page, and the resulting refresh token lands in a gitignored `.decanter-auth.json` (rotated automatically on every refresh). No browser or piped run? Paste an **MCP token** instead (minted in n8n → Settings → MCP → API key; stored as `N8N_MCP_TOKEN` in `.env`). - Offers the **optional public API key** (`N8N_API_KEY`) — only needed for [executions](/docs/cli/executions/), [data-tables](/docs/cli/data-tables/), and [backup](/docs/cli/backup/). - When credentials already exist they are reused — edit or delete `.env` / `.decanter-auth.json` to change them. A best-effort connection check runs at the end (it also reports how many workflows are already "Available in MCP"). - Copies the starter template. Files named `X.example` in the template land as `X` in the target, and a copy-time baseline is recorded in `.decanter-template.json` (see [Re-running init](#re-running-init)). - Scaffolds `decanter.config.json` and a `.gitignore` (which covers `.env` and `.decanter-auth.json`). - Closes by pointing at **n8n's official skills pack** — see [The n8n skills pointer](#the-n8n-skills-pointer) below. The instance needs MCP access enabled once (n8n → Settings → MCP; ~2.20+), and each workflow you sync needs its "Available in MCP" flag — see [configuration](/docs/concepts/configuration/). ## Non-interactive setup (`--host` / `--token` / `--api-key`) Passing **any** of `--host`, `--token`, or `--api-key` runs `init` non-interactively — values come from the flags plus any existing `.env`, and **no prompt is ever issued** (so it drives cleanly from a script or a coding agent, with no stdin dance): ```sh n8n-decanter init --host http://localhost:5678 --token "$N8N_MCP_TOKEN" n8n-decanter init ./flows --host n8n.example.com --token "$TOK" --api-key "$KEY" ``` - `--host ` — the n8n origin. Normalized like a typed host (a scheme-less local address gets `http://`, everything else `https://`; a scheme you write is kept). **Required** in this mode — omit it and `init` errors instead of prompting. - `--token ` — the MCP bearer token (`N8N_MCP_TOKEN`), the same one the paste path uses. Omit it and `init` writes the rest and warns that sync won't work until credentials are set (there is **no** headless OAuth — the browser consent flow needs a terminal). - `--api-key ` — the optional public API key (`N8N_API_KEY`). Omit it and it's simply skipped. An explicit flag wins over an existing `.env` value; the end-of-init connection checks run exactly as they do interactively. `--force` composes with all three. ## The n8n skills pointer decanter owns Code-node source; **[n8n's official skills pack](/docs/agents/n8n-skills/)** teaches your agent everything else. A **first** `init` closes by naming it and printing the install commands for the agent it detects: ```text Recommended: n8n's official skills pack (n8n-io/skills) — it teaches your agent to build workflow structure over MCP while decanter keeps every Code node a file. Claude Code (detected) claude plugin marketplace add n8n-io/skills claude plugin install n8n-skills@n8n-io then /reload-plugins (or restart Claude Code) Codex codex plugin marketplace add n8n-io/skills codex plugin add n8n-skills@n8n-io then restart Codex and approve the plugin's hooks (needs Codex >= 0.142.0) other agents (skills.sh) npx skills add n8n-io/skills -y no plugin hooks on this route — the scaffolded AGENTS.md carries the routing cue it needs guide: /docs/agents/n8n-skills/ ``` The `(detected)` marker comes from your environment (running inside an agent, its binary on `PATH`, or a `~/.claude` / `~/.codex` marker) and only decides which route is listed first — every route is always shown. **`init` prints; it never installs.** Running `claude`/`codex`/`npx skills` for you would mean decanter spawning three third-party CLIs with their own version floors, mutating agent state that lives outside the sync dir, at the most fragile moment of setup — and a plugin installed mid-session isn't active until the agent reloads anyway, so the subprocess buys nothing the printed command doesn't. It is printed **once**, on a first init (before `.decanter-template.json` exists); every re-run stays quiet, so there is no flag to turn it off. Piped and `--host`-driven runs get it too — an agent bootstrapping a sync dir should learn the pack exists as much as a human does. ## TypeScript tooling `init` also scaffolds what a sync dir needs to type-check and run nodes locally: a `package.json` (with a `typecheck` script and the `typescript` devDependency), `tsconfig.json`, and `n8n-globals.d.ts` with types for the Code-node globals (`$input`, `$('…')`, `DateTime`, …). Verification routes through the CLI, so `n8n-decanter` must be on the sync dir's PATH: install it globally, add it to the sync dir's `devDependencies`, or `npm link` a git checkout (build it first — Node won't type-strip `.mts` under `node_modules`). ## Agent configs The template includes an `AGENTS.md` contract for coding agents plus per-agent configs (Claude Code, Cursor, Codex, opencode), including a hook that runs `preflight --offline` after node edits — see [Agents](/docs/agents/overview/). The scaffolded MCP config (`.mcp.json` / `opencode.json`) wires two servers out of the box: **`n8n-instance`** — your instance's full MCP surface through the [mcp connect](/docs/cli/mcp-connect/) guard (structure and lifecycle acts pass; Code-node `jsCode` writes are blocked toward the file + push flow) — and **`n8n-docs`**, n8n's official read-only docs MCP. ## Re-running init `init` is safe to re-run — for example to pick up template improvements after upgrading the CLI. It's **modification-aware** (like dpkg conffiles): at first init it records the hash of every template file in a git-tracked `.decanter-template.json` manifest, then compares that baseline against your working copy and the current template on each re-run: - **Files you haven't touched** whose template version changed → `init` lists them and offers to update (a single `y/N` confirm). Non-interactive runs report that updates are available and apply nothing — re-run interactively or use `--force`. - **Files you've edited locally** → left untouched; reported as `left unchanged (modified locally): …`. - **Files changed in both the template and your copy** → left untouched; flagged as a conflict to resolve manually (or `--force` to take the template version). - **Files new to the template** → copied in. - **Files the template *renamed*** → migrated, never silently doubled. Your copy is removed and re-scaffolded under the new name if you hadn't touched it; if you had, it is left exactly where it is and the new name is **not** written (two overlapping settings files would fire their hooks twice) — `init` tells you to move it, and picks up where you left off next run. A file `init` never wrote is always left alone. `--force` resolves a pending rename by removing the old file, per its "reset everything" contract. The one rename so far: **`.claude/settings.local.json` → `.claude/settings.json`** (it holds shared project policy, not per-machine preferences — see [Agents](/docs/agents/overview/)). Commit `.decanter-template.json` — it's the shared baseline, so a teammate who clones and re-inits sees the same drift picture. `.env` is never tracked in it. ## Flags - `--force` — the escape hatch: overwrites **every** template file with its template version, including ones you edited (each such file is flagged `(had local changes)`), then re-records the baseline. `.env` is never touched. --- # pull Source: https://buttjer.github.io/n8n-decanter/docs/cli/pull/ ```sh n8n-decanter pull [workflow…] ``` Pulls each workflow into `workflows//` over n8n's MCP server: one source file per Code node under `code/`, plus the read-only `workflow.json` structure snapshot with each Code node's `jsCode` replaced by a `//@file:` placeholder — see [Sync layout](/docs/concepts/sync-layout/). **Without a ref**, on a terminal `pull` opens an interactive picker listing your workflows — local *and* remote (over MCP) — so you can pick one without knowing its id; picking a not-yet-local one pulls it fresh. Already-pulled workflows are listed **newest-synced first** (the one you last pulled or pushed is under the cursor), with remote-only rows after them. Piped or non-interactive, it instead pulls the workflows listed in [config](/docs/concepts/configuration/). `pull` also resolves a name/id it doesn't know locally against the server's workflow list, so you can pull a new workflow by name. Pull reads the workflow **tip** — what the n8n editor shows: the unpublished draft when one exists, else the published content. A workflow must have **"Available in MCP"** enabled (workflow card ⋯ menu, or workflow settings) before it can be pulled; the error tells you where the switch lives, and [`list --remote`](/docs/cli/list/) marks gated workflows. After a successful pull the folder is git-committed automatically (`"commitOnPull": false` disables it). ## What pull never touches `.ts` node sources are one-way — pull never modifies them. Remote changes to a TS-managed node (for example a UI edit) are **warned about**, not merged: inspect them with [`diff`](/docs/cli/diff/) and port what you want to keep into the `.ts` by hand. See [TypeScript nodes](/docs/concepts/typescript-nodes/). ## Pull re-baselines the sync state Pulling records the remote code as the new sync base — **after a warned pull, the next push overwrites the surfaced remote edits by design**, with [`diff`](/docs/cli/diff/) and git history as the safety net. `.js` files are overwritten with the remote body, and pull warns when that clobbers unpushed local edits. **git really is the safety net here**: pull takes a **snapshot commit before it writes anything**, so an uncommitted local edit is recoverable from that commit (`commitOnPull` must be on — it is by default). If the snapshot cannot be made (no git repo, `commitOnPull: false`, a git error), the pull still runs but the warning says so instead of promising a recovery that does not exist. ## Renames and migrations Node **ids are stable across renames** (wherever the rename happened — UI, MCP), and the id-keyed state maps each node to its file: pull follows renames by moving the local file to the new kebab-case name. Layouts from older versions (node files at the folder root) migrate automatically on the next pull. **A rename over MCP leaves `$('…')` references behind, and pull cannot fix them.** The n8n editor rewrites every `$('Old Name')` reference when you rename (in the browser, before it saves). n8n's `renameNode` MCP op does not — it rewrites the node name and the connections, reports success, and leaves the references dangling in Code-node source *and* in other nodes' expression parameters. Pull mirrors what n8n stored, so it copies the dangling references down; the compliance guard then blocks `push` until they are repaired (`preflight --offline` names each one). Repair them **in this order**: other nodes' expression parameters first, in n8n (`updateNodeParameters` over MCP, or the editor) — then the local code files, then `push`. The other order loses the code fix: an MCP write triggers a background snapshot refresh whose pull overwrites unpushed `.js` edits. --- # push Source: https://buttjer.github.io/n8n-decanter/docs/cli/push/ ```sh n8n-decanter push [workflow…] [--force] [--publish] [--no-typecheck] ``` Compiles and uploads each workflow's **Code-node source** over n8n's MCP server — one atomic batch of `jsCode`-only updates, addressed to each node by its current name (ids anchor the mapping, so renames made elsewhere don't matter). `.js` node files are pushed verbatim (byte-lossless); `.ts` files are compiled with esbuild and their imports from `shared/` and opted-in npm packages are bundled in — see [TypeScript nodes](/docs/concepts/typescript-nodes/). Structure is never pushed — `workflow.json` is a read-only snapshot. **Every push lands on the workflow's draft.** The live (published) version does not change until [`publish`](/docs/cli/publish/) — or `push --publish`, which publishes right after a successful push. n8n keeps running the published version in between. After a successful push the folder is git-committed automatically (`"commitOnPush": false` disables it). ## The gates Push runs three checks, in order — [push gates](/docs/concepts/push-gates/) has the full rules: 1. **Typecheck** — blocking; `--no-typecheck` skips it (auto-skipped when no `tsconfig.json` is found). 2. **Compliance guard** — layout violations are hard errors that `--force` does **not** bypass. The full list is under [preflight](/docs/cli/preflight/#what-the-compliance-guard-catches), which runs the same guard as its `layout` check. 3. **Per-node drift guard** — a Code node's remote code changed since the last sync → abort with `pull first`. Only this gate is bypassed by `--force`. Remote *structure* changes never block a push. ## Flags - `--publish` — take the draft live after a successful push (also publishes when there was nothing new to push). - `--force` — bypass the per-node drift guard. It overrides the protection for code edited on the instance — don't use it casually, and never let an agent use it unasked. - `--no-typecheck` — skip the typecheck gate. --- # diff Source: https://buttjer.github.io/n8n-decanter/docs/cli/diff/ ```sh n8n-decanter diff [workflow…] ``` Shows the **actual changed lines** between your local Code-node source and the workflow's draft on n8n: `--- remote (n8n)` / `+++ local ()` and `@@` hunks, per node — exactly what a [push](/docs/cli/push/) would overwrite, and exactly what a [pull](/docs/cli/pull/) would bring in. It reads the instance and writes nothing. ## The `git status` / `git diff` split `diff` is the **`git diff`** half of code sync: the lines. The **`git status`** half — *is anything pending, is anything drifted, is the live version behind* — is [`preflight`](/docs/cli/preflight/), which turns the same facts into a scored verdict. Both read one shared fact computation, so they can never disagree about which nodes moved. ```sh n8n-decanter preflight order-sync # the summary + the verdict (the gate) n8n-decanter diff order-sync # the lines behind that summary ``` ## It always exits 0 `diff` is an inspection view, not a gate — like `git diff`, its exit code says nothing about whether you should ship. It exits **0** whether every node matches or every node conflicts. (A genuine failure — an unreachable instance, an unavailable workflow — still exits 1, because the command didn't run.) **This drops the CI exit codes the retired `status` verb had.** A pipeline that gated on `status` migrates to `preflight`, which is the verb that grades: ```sh n8n-decanter preflight --json # verdict + exit 1 on not-ready n8n-decanter preflight --fail-on=warn # also exit 1 on a caution ``` ## Refs, multi-workflow, and the picker `diff` takes refs like `pull`/`push`: several at once, each rendered under its own header, or none — in which case a terminal opens the [picker](/docs/cli/overview/#interactive-picker) and a piped run falls back to the `"workflows"` list in your [config](/docs/concepts/configuration/). Each workflow's block starts with ` () []`. A ref that isn't pulled yet is reported as such and skipped, not treated as an error. ## What it prints — and what it omits **In-sync nodes are omitted entirely.** Only nodes that differ get a line, so a clean workflow prints one line: ```txt Order Sync (wf123) [workflows/order-sync] no differences — every tracked node matches the draft ``` Everything else gets a state line, then the diff: | State line | Means | | --- | --- | | `local changes in code/ — push pending` | you edited locally; the draft is still at the last sync | | `changed remotely — pull` | the draft moved; your file is still at the last sync | | `CONFLICT — changed both locally and remotely` | both sides moved off the last sync | | `local file code/ missing` | the file `.decanter.json` tracks is gone from disk | | `remote code node unknown locally — pull` | a Code node exists on n8n with no local state entry | | `code/: node deleted remotely` | the node this file belongs to is gone from the workflow | The last three have only **one** side to show, so they print the state line without a hunk. Everything else prints the unified diff underneath. ## `.ts` nodes are compiled before comparing For a `.ts` node the local side of the diff is the **compiled** JavaScript — the exact bytes `push` would send, `shared/*.ts` imports bundled in. So editing one shared helper shows up as a diff in **every node that imports it**, which is the honest answer to "what does this helper change touch?". See [TypeScript nodes](/docs/concepts/typescript-nodes/). Compile warnings for a node are replayed immediately above that node's diff. ## What `diff` deliberately does not tell you Three facts the retired `status` verb printed are **not** here — they aren't line diffs, and each survives as a [`preflight`](/docs/cli/preflight/) check: | Fact | Now | | --- | --- | | published / unpublished, and whether the live version lags the draft | the `lifecycle` check | | `workflow.json` structure snapshot out of date | the `snapshot` check | | the roll-call of nodes that *are* in sync | the `parity` check | --- # watch Source: https://buttjer.github.io/n8n-decanter/docs/cli/watch/ ```sh n8n-decanter watch [workflow] [--force] ``` Watches one workflow's `code/` files and pushes each save **to the workflow's draft** over n8n's MCP server (needs exactly one workflow — pass a ref, or list a single workflow in the config). Runs until Ctrl-C. Nothing goes live during a watch session: run [`publish`](/docs/cli/publish/) when the code should ship. ## Session start: safety commit + pull Every watch session starts with a safety commit and a pull, so the session has a clean baseline and nothing uncommitted can be lost to an incoming change. ## workflow.json is a read-only snapshot Saving `workflow.json` pushes nothing — structure lives in n8n. Watch warns once per session if you edit it, and the next pull overwrites the file. ## The editor updates live Keep the n8n editor tab open on the watched workflow — n8n 2.x reflects each push in the open canvas natively (no proxy, no manual refresh), and skips the update if the tab has unsaved edits so nothing in-browser is clobbered. Node saves are guarded by the same [compliance rules](/docs/cli/preflight/#what-the-compliance-guard-catches) as a manual push, so a broken save doesn't reach n8n. `--force` carries through to the per-node drift guard, exactly as on [push](/docs/cli/push/). --- # publish / unpublish Source: https://buttjer.github.io/n8n-decanter/docs/cli/publish/ ```sh n8n-decanter publish [workflow…] # take the draft(s) live n8n-decanter unpublish [workflow…] # return to draft-only ``` n8n 2.x splits each workflow into a **draft** and a **published** version. In the editor, *Save* updates the draft and *Publish* makes it live. Every decanter [push](/docs/cli/push/) updates the **draft only** — these verbs are the deliberate go-live half: - **`publish`** takes the draft live — the code runs from now on. On a published workflow whose draft has diverged (pushes, or UI edits), it promotes the newer draft. - **`unpublish`** returns the workflow to draft-only. Both go over n8n's MCP server. Without refs they act on the workflows listed in `decanter.config.json`. `push --publish` combines a push with the publish in one command. ## The go-live gate `publish` refuses a draft carrying a dangling `$('…')` reference — one that names a node the workflow doesn't have. That reference fails at run time, so publishing it would put a known break into production. The check runs against **the draft on the instance**, not your local folder. That is deliberate: `workflow.json` is a snapshot, so grading it here would pass a broken workflow whenever the local mirror is out of date, and block a legitimate publish from a fresh clone. It costs nothing extra — `publish` already reads the draft to decide what to do. It is the same check [`test`](/docs/cli/test/) runs bare, and the message names both halves and the order to repair them in. Running it in both places is not redundant: the instance can change between the two, so only the check inside `publish` is authoritative for that publish. The usual cause is a rename — n8n's `renameNode` MCP op rewrites the node name and the connections only, and leaves every reference behind. See [`pull`](/docs/cli/pull/#renames-and-migrations). ## Already in that state Running `publish` when the live version already equals the draft (or `unpublish` on an already-draft workflow) is a **no-op with a note**, not an error — nothing changes and the command still exits 0. ## The standard loop ```sh n8n-decanter push wf # update the draft (live version untouched) # …iterate, test, repeat… n8n-decanter publish wf # ship it — or use push --publish for the last one ``` Because pushes never auto-publish, there is no need to `unpublish` first for a staged rollout — the draft accumulates changes while the published version keeps running. --- # preflight Source: https://buttjer.github.io/n8n-decanter/docs/cli/preflight/ ```sh n8n-decanter preflight [workflow…] [--simulate] [--offline] [--viewer] [--json] [--fail-on=warn] [--fail-fast] [--require=] [--no-typecheck] [--no-fetch] [--execution | --scenario ] [--n8n-version ] ``` **`preflight` verifies your local code** — local static → instance read-only → local-engine replay — ordered fast→slow, and condenses them into a scored verdict with actionable feedback for humans *and* agents. It is the whole verify surface: the layout-compliance guard, the typecheck, the sync/drift summary, and the offline engine replay all live here, behind two flags. **It never runs your workflow on the n8n instance.** That is [`test`](/docs/cli/test/)'s job, and it belongs *after* the push — see [the flow](#the-flow-preflight--push--test--publish) below. For the *changed lines* rather than the summary, use [`diff`](/docs/cli/diff/). With no workflow it runs every workflow in your config (or opens the [picker](/docs/cli/overview/#interactive-picker) on a terminal); when the config lists none, it falls back to **every pulled workflow**, so a fresh scaffold still gets a whole-project gate. The exit code aggregates across them. ## The flow: `preflight` → `push` → `test` → `publish` ```sh n8n-decanter preflight # 1. is my local code sound? (local) n8n-decanter push # 2. make it the draft (writes the draft) n8n-decanter test # 3. grade the draft on n8n (static; pinned run with a flag) n8n-decanter publish # 4. go live (publishes) ``` The order is the point. `test` runs n8n's **draft**, so before step 2 the draft is not your code — an instance run at step 1 would grade something you aren't shipping. Each step verifies the artifact the previous step produced: | Step | Grades | Touches n8n? | | --- | --- | --- | | `preflight` | your local files | reads only | | `push` | — | **writes the draft** | | `test` | the draft, which is now your code | reads it; **runs the draft** with `--execution`/`--scenario` | | `publish` | — | **publishes** | `preflight` is deliberately the only step that changes nothing. It is safe to run on every save, in a hook, or in CI. ## Depth: two flags, no profiles ```sh n8n-decanter preflight # static + instance reads (the default gate) n8n-decanter preflight --simulate # + a local-engine run of your code (Docker) n8n-decanter preflight --offline # static only — no instance contact n8n-decanter preflight --offline --simulate # static + local engine, still no instance ``` **`--simulate` is additive, `--offline` is subtractive, and they compose.** | Invocation | Tiers | For | | --- | --- | --- | | `preflight` | static + sync | the pre-push gate | | `preflight --simulate` | static + sync + runtime | maximum coverage before a push | | `preflight --offline` | static | an edit hook, an air-gapped CI lint | | `preflight --offline --simulate` | static + runtime | air-gapped runtime evidence | > **`--offline` no longer implies the engine replay.** It used to; now it does > exactly one thing — drop the instance tier. The old `--offline` behaviour > (static + a local engine run, no instance) is > **`preflight --offline --simulate`**. The `--full`, `--quick`, and > `--default` profile flags are gone with the vocabulary: `--full` is > `--simulate`, `--quick` is `--offline`. They are **unrecognized, not > rejected** — the CLI ignores unknown flags, so `preflight --full` silently > runs the default gate with no engine. Update any CI job that passes one; > nothing will warn you at runtime. Nothing escalates on its own. An auto-escalating variant (run the engine only "when it would add signal") was **rejected** — surprise Docker boots and nondeterministic wall time are worse than one explicit flag. Every skipped check prints its reason and its unlock, so a run is never silently narrower than it looks. ## The ladder — every check, fast → slow Each check has a **stable id** (agents key on it). | Tier | Check | Verifies | Can produce | | --- | --- | --- | --- | | **static** (offline, ms) | `layout` | the [compliance guard](#what-the-compliance-guard-catches): placeholders, connections, duplicate names/ids, orphans, dangling node refs (`$('…')`, `$node[…]`, `$node.X`, `$items(…)`) | fail / warn | | | `types` | [typecheck](#typecheck) of the node files | fail / skip | | **sync** (instance, read-only) | `connect` | MCP reachable, auth valid (exercises OAuth refresh) | fail | | | `access` | workflow is *Available in MCP* | fail | | | `parity` | local code == the draft, node by node — i.e. is a `push` pending | pass / warn | | | `drift` | remote code moved off the last sync — someone edited on the instance | warn / **fail on CONFLICT** | | | `snapshot` | the `workflow.json` structure snapshot still matches n8n | pass / warn | | | `lifecycle` | published or unpublished, and whether the live version lags the draft | info | | | `history` | recent production runs: error rate, most recent failure | warn | | | `capture` | a capture/scenario exists to pin from, and matches the draft | warn / info | | **runtime** (executes locally, minutes) | `simulate` | [pinned replay](#the---simulate-stage) of **local** code on a **local** engine, per-node diff | fail / skip | Checks **stream as they complete**, so a fast red surfaces in the first second even when the runtime tier takes minutes. `--fail-fast` stops after the first failure (the rest are skipped, and say so); the default always completes the card. `capture` is a local read of the `executions/` dir, so it is evaluated even under `--offline`; the rest of the sync tier is skipped with *`--offline` skips the instance tier*. ### What the sync-tier rows actually report - **`parity` — is a push pending?** Compares each tracked node's local build against the draft body. All matching is a `pass` ("local code matches the draft"). Otherwise it's a `warn` naming the count, with every node listed in `details` and `push` as the remediation — not a caveat, just the next step in the flow. A tracked file that has vanished from disk warns differently (*a local file is missing* — pull, or push to make the draft match local). - **`drift` — did someone edit on the instance?** Remote code that moved off the last sync while your file didn't is a `warn` (*pull before publishing*). Both sides moved is a **`CONFLICT` fail**, with [`diff`](/docs/cli/diff/) as the remediation so you can see the lines before choosing. Nodes deleted remotely count here too. - **`snapshot` — is `workflow.json` current?** Structure is mirrored, not guarded: when the remote structure changed, this warns *structure snapshot out of date — pull to refresh `workflow.json`* (and, if the file can't be parsed, *unreadable — pull to rewrite the snapshot*). It never fails the gate — a stale snapshot is a hint to refresh a mirror, not drift in your code. - **`lifecycle` — where is this workflow in its life?** Always `info`, never a gate: *unpublished — draft only*, *published — live matches the draft*, or — when a published workflow's draft has moved ahead (pushes land on the draft, and so do UI edits) — *published — the live version is older than the draft (publish to go live)*, which is how you learn a [publish](/docs/cli/publish/) is pending. ## What the compliance guard catches The `layout` check is the same guard [push](/docs/cli/push/) and [watch](/docs/cli/watch/) run before writing — removing the old standalone `check` verb removed a *view*, not a gate. Hard errors (`--force` does **not** bypass them): - inline code in `workflow.json` without a `//@file:` placeholder - placeholders pointing at missing, `.remote.js`, or non-`.js`/`.ts` files, or at files outside `code/` - an `@ts-n8n` marker inside a `.js` file - an `import` in a `.js` node file — `.js` is pushed verbatim and n8n has no module loader; convert the node to `.ts`, where imports are bundled on push - dangling connection sources/targets - duplicate node names or ids - orphan `.js`/`.ts` files nothing references - dangling node references, in node source and in expression parameters — all four forms n8n itself rewrites on a rename: `` $('X') ``, `$node["X"]`, `$node.X`, `$items('X')`. Computed references (`$(someVar)`, a template literal carrying `${…}`) are left alone; a regex cannot resolve them, and n8n's own rewriter has the same limit - a leftover legacy `fixtures/` dir containing `.json` files — the per-node fixtures mechanism and the old `--pin` flag are retired; recreate the data as a [scenario](/docs/cli/scenario/), then delete the dir Warn without blocking: **local work not yet registered with the instance** — a node whose `//@file:` placeholder has moved off what `.decanter.json` records (the shape of a `.js`→`.ts` conversion), or whose recorded file is gone from disk. `push` reconciles the map, so this is a pending sync, not a violation — and it stays a warning deliberately, because `push` runs this guard *before* it reconciles. Also: unresolved `.remote.js` leftovers; a Python Code node's inline `pythonCode` (decanter extracts JS/TS only — Python extraction is planned); and a committed scenario whose `workflowData` embeds inline Code-node source (`jsCode` not starting with `//@file:`). The one-line `layout` message names the first violation and the count; **every** error and warning is listed underneath it in [`details`](#details--the-full-list-behind-a-line). ## Typecheck n8n Code-node source is a *function body* (top-level `return`/`await`), which plain `tsc` rejects. The `types` check wraps node files in an `async function` in memory and maps diagnostics back to real line numbers — see [Type checking](/docs/concepts/type-checking/) for how this works and why your editor may still show a spurious TS1108. `npm run typecheck` in a scaffolded sync dir is an alias for this. Every `tsc` line lands in the finding's `details`. `--no-typecheck` skips the check (it reports as a skip with the unlock, so the coverage line stays honest), and a sync dir with no `tsconfig.json` skips it automatically. > **Green means well-formed, not live.** `preflight --offline` never contacts > the instance, so a `ready` verdict there says your files are valid — not that > n8n is running them, and not that the draft matches. Drop `--offline` to add > the instance reads (`parity`, `drift`, `lifecycle`), use > [`diff`](/docs/cli/diff/) to see the pending lines, and > [`push`](/docs/cli/push/) to make your edits real. Editing and then stopping > at a green offline run leaves the workflow unchanged in n8n. ## The `--simulate` stage `--simulate` replays the workflow through a **real n8n engine**, locally, using a captured execution (or a committed [scenario](/docs/cli/scenario/)) as the pinned input. Side-effect-free nodes (Set, IF, Switch, Merge, Code, …) **execute for real** through the actual engine; every network/side-effectful node is **pinned** to the output it produced in the capture. Credentials are stripped and no outbound-capable node survives the transform, so the run is dry — it writes nothing external. Then each real node's replayed output is **diffed against the capture**: divergence is the check's `fail`. It needs a **Docker** daemon (no daemon → the check skips with that reason) and a pin source. It is the one thing an instance run can't give you: verification of **uncommitted local code** ([`test`](/docs/cli/test/) can only run what's on the draft), CI without an instance or credentials or the per-workflow MCP opt-in, hard network isolation, and engine-version rehearsal. **How it works:** transform a copy of the workflow (materialize `//@file:` sources, replace the trigger and every network node with a name-preserving node that emits the captured items so `$('Node')` and expressions still resolve, prepend a manual trigger, strip all `credentials`) → run it on a throwaway n8n (`n8n import:workflow` + `n8n execute`) in a fresh container with no server, no credentials and its own scratch database → diff each executed node against the capture. Only nodes on a curated, **default-deny** allowlist run for real; any node type not on it — anything credentialed, HTTP, DB, messaging, or unknown — is pinned. Safety never depends on recognizing a node type. Loop drivers (`splitInBatches`) are the exception: side-effect-free but stateful across runs, so they run for real to reproduce the loop. ### Pin sources - `--execution ` — replay that captured execution ([executions](/docs/cli/executions/) fetches them into the gitignored `executions/` dir). - `--scenario ` — replay a committed [scenario](/docs/cli/scenario/) (`scenarios/.json`). Mutually exclusive with `--execution`. - Neither — the **newest capture** in `executions/`, so `--simulate` just works after a fetch. No capture and nothing to pin from → the check skips, naming both ways to get one. A **gap** — a network node reached in the replay with no captured or pinned data, typically a node added since the capture — hard-errors rather than run half-real. Fill it by promoting the capture to a [scenario](/docs/cli/scenario/) and authoring the missing node's data (or scaffolding its schema with `scenario create --scaffold`; the CLI never calls a model). **Synthetic pins are the exception to the diff.** A scenario containing any `authored`/`scaffolded` node (see [provenance](/docs/cli/scenario/#provenance-and-synthetic-pins)) passes as "**synthetic pins — proves executability, not output correctness**": no per-node diff is asserted. A capture-only run keeps the full diff semantics. Nodes with **nondeterministic** output (`$now`, `Math.random()`, `new Date()`) legitimately diverge — that's a real signal, not masked. ### Engine version "Engine-true" means true to *your* instance, so the engine version is a parameter. Set `n8nVersion` in `decanter.config.json` (or `--n8n-version` for one run) to match your n8n: ```json { "n8nVersion": "2.31.4" } ``` Absent that, the stage uses the project's pinned version and hints you to set one. `--n8n-version` affects **only** the `--simulate` engine — which is what makes it an **upgrade rehearsal**: run your workflow on the next n8n before the instance gets there. The consumed surface (`import:workflow`, `execute`, the run-data JSON) is stable across the n8n 2.x line. ### `--viewer` — browse the run in a real n8n `--viewer` (only valid together with `--simulate`; alone it's a hard error) additionally starts a **browsable throwaway n8n** and prints, in the check's `details`, a URL to the run plus the local login: ```txt ✓ simulate 6 node(s) ran on a local engine, all matched the capture open the run in n8n: http://127.0.0.1:53737/workflow/decantersim0000/executions/1 local login: simulate@decanter.local / Decanter-Sim-0000 — throwaway instance, replaced on the next run ``` - The viewer is a **second, separate container** alongside the graded run. The graded run stays headless with **`--network-none` forced on**, so the [safety contract](#safety-contract) is unchanged by `--viewer`. - It is bound to `127.0.0.1` only, holds no credentials, and is replaced on the next run. n8n requires a login, so it seeds a fixed local owner and prints it — log in once and the browser session sticks. Stop it any time with `docker rm -f decanter-sim-viewer`. - Booting it takes 30–180 s; its progress is printed (a swallowed boot would read as a hang). - It's also what the [picker](/docs/cli/overview/#interactive-picker)'s `preflight --simulate` row runs. **`--viewer` is the only way to see a multi-batch loop.** A loop that ran more than one batch can't be gated — first-run-only pinning can't feed iterations 2..N — so without `--viewer` the stage **fails** (*loop workflows are out of scope (v1)*). With `--viewer` the loop is capped to its first batch, replayed, and opened in the browser, and the check reports **`skip`**: *a preview, not a pass/fail check*. It is never a pass, so `--require=simulate` rightly fails on it and nothing can misread the exit code as verified. (A **single**-batch loop — the driver ran twice, one batch pass plus the final "done" pass — replays faithfully and is a real pass/fail check.) ### Not a replacement for `node run` [node run](/docs/cli/node-run/) is the sub-second inner loop — one node, in-process, zero install. `--simulate` is the slow outer check — the whole graph, a real engine, needs a capture. (One inversion worth knowing: `run` executes node code in the CLI process with full host privileges, while the simulate engine runs it inside n8n's sandbox with the network cut — for generated or untrusted node code, `--simulate` is the safer executor.) ## Executions are the ground truth `preflight` brings your real run data into the gate: - **Pins and diffs.** The runtime tier pins from and diffs against a capture (`--execution `, default newest) or a committed [scenario](/docs/cli/scenario/) (`--scenario `). - **Auto-fetch.** When `--simulate` runs *and* the instance tier is live (i.e. `--simulate` without `--offline`) and `N8N_API_KEY` is set, `preflight` fetches the newest capture if the local one is missing or stale, so the replay pins against *fresh* reality. It's a read (captures land in the gitignored `executions/` dir); `--no-fetch` disables it, and without a key it's skipped with guidance. Without `--simulate` nothing consumes a capture, so nothing is fetched; `--offline` never contacts the instance at all. - **History as a health signal.** The `history` check reads recent production executions (over MCP `search_executions`, or the REST executions API when `N8N_API_KEY` is set) and reports the error rate — a live workflow that's been failing is a **warn**, never a fail (the draft isn't guilty of the past). ## Scoring & verdict Each check reports `pass` / `warn` / `fail` / `skip` / `info`, a duration, a message, optional `details`, and — for a warn or fail — the exact **remediation** command. - **Verdict** (deterministic): any `fail` → **`not ready`** (exit 1); else any `warn` → **`caution`** (exit 0); else **`ready`** (exit 0). `--fail-on=warn` promotes a caution to exit 1. Exit codes stay 0/1. - **Score 0–100** (the trend line; the verdict is the gate): starts at 100, each `fail` costs 40 (a `CONFLICT` `drift` costs 30), each `warn` costs 10, floored at 0. The weights are starting values, tuned freely; the verdict rules are the stable contract. - **Coverage is first-class honesty.** The card always says which checks ran vs skipped and why — a 100 with no runtime run reads as `ready` with the coverage gap named, never a bare green. **`--require=`** (a comma list of check ids, e.g. `--require=simulate`) turns a *skip* of that check into a **fail** — the CI teeth for "must have runtime coverage". `--require=test` is rejected with a pointer to the flow: the instance run is no longer a preflight stage. ### `details` — the full list behind a line A check line is a summary; `details` is the expansion, printed indented and dim beneath it (and carried in `--json`). It holds **every** layout violation and warning, **every** `tsc` diagnostic, the drifted/conflicted node list, the diverged nodes' expected-vs-actual, and the viewer's URL + login. Since `preflight` is now the only place these are printed, nothing is lost by there being no separate static-check verb. `layout` and `types` findings deliberately carry **no `remediation`**: that field is contracted to hold a runnable command, and the fix here is editing the files named in `details`. ## The report — for humans and agents The human card streams a line per check (with its details indented under it), then the score/verdict/coverage and the remediation for anything that warned, failed, or was skipped: ```txt preflight: order-sync · static + instance reads ✓ layout layout compliant ✓ types node files typecheck clean ✓ connect MCP reachable, auth valid ! parity local code differs from the draft in 1 node(s) — push to make it the draft, then test Compute Totals: local changes in code/compute-totals.ts ✓ drift no remote code drift score 90/100 · verdict: caution · 10/11 checks ran ! parity: local code differs from the draft … → n8n-decanter push order-sync ⤷ skipped simulate: the local-engine replay is opt-in — pass --simulate to run it (needs Docker) ``` The header's suffix is the resolved depth: `static + instance reads`, `--offline`, `--simulate`, or `--offline --simulate`. `--json` emits **one document** (an array when several workflows are targeted): `workflow`, `id`, `flags` (`{simulate, offline}`), `subject` (`draftVersionId`, `publishedVersionId`, `parity`), `checks[]` (`id`, `tier`, `status`, `message`, `details?`, `remediation`, `durationMs`), `score`, `verdict`, and `coverage` (`ran`, `skipped[] {id, reason, unlock}`). > **Agent contract change.** `report.profile` (a string) is **gone**, replaced > by `report.flags` — an object with the two booleans. Findings may now carry > `details: string[]`. The stable check ids + remediation strings are otherwise > unchanged: teach an agent `preflight --json` as its gate before `push` (the > gate before `publish` is [`test`](/docs/cli/test/), run after the push). ## Safety contract **`preflight` never mutates and never executes on your instance:** no push, no publish, no restore, no draft write — and no `test_workflow` run. Its only instance interactions are **reads**. - Every stage that produces a verdict grades your **local code**. The instance is read for sync facts (`parity`, `drift`, `snapshot`, `lifecycle`, `history`) and nothing else. One report, one artifact. - The `parity` warn is not a caveat about coverage — it's the next step in the flow: `push`, then `test`. - The graded `--simulate` run is always headless in a throwaway container with **`--network-none` forced on** and credentials stripped. `--viewer` does not relax that; it adds a *separate* container to look at. - The sync tier and auto-fetch are reads only; captures land in the self-gitignored `executions/` dir. Auto-fetch only runs when `--simulate` is on and the instance tier is live; nothing else consumes a capture. ## Preflights — which one when? `preflight` is the umbrella; the flags pick the depth. [`test`](/docs/cli/test/) is **not** under this umbrella — it is a separate, later step: | | Where it runs | Reach for it when | | --- | --- | --- | | `preflight --offline` | locally, static | every edit, an edit hook, an air-gapped lint — layout + types | | **`preflight`** | + instance reads | **before `push`** — one command, one verdict | | `preflight --simulate` | + a local engine (Docker) | runtime evidence about **local** code before pushing | | [diff](/docs/cli/diff/) | locally + one instance read | you want the changed **lines**, not a verdict (never gates) | | [test](/docs/cli/test/) | your instance | **after `push`** — a real run of what you just pushed | Requirements match the checks it runs: the static tier needs nothing; the sync tier needs the [MCP connection](/docs/cli/init/) and the workflow's *Available in MCP* flag; `--simulate` needs Docker; auto-fetch and the REST `history` fallback need `N8N_API_KEY`. Anything unavailable is **skipped with an unlock**, never a hard error — a workflow with zero captures still gets a static+sync verdict, labeled as such. --- # test Source: https://buttjer.github.io/n8n-decanter/docs/cli/test/ ```sh n8n-decanter test [--execution | --scenario ] [--trigger ] [--json] ``` `test` grades **the draft on your instance** — the thing [`publish`](/docs/cli/publish/) would take live. It has two tiers. ## Bare: the static tier — nothing runs ```sh n8n-decanter test ``` Reads the draft and checks it for dangling `$('…')` references, in Code-node source *and* in other nodes' expression parameters. **It executes nothing** and needs no capture, no scenario, and no pinning — so it always works, including on a fresh clone. This is the cheap half of the same question the pinned run asks, and it is what [`publish`](/docs/cli/publish/) refuses on. The usual cause of a finding is a rename: n8n's `renameNode` MCP op rewrites the node name and the connections only and leaves every reference behind (the n8n editor does rewrite them). The output names both halves and the order to repair them in — see [`pull`](/docs/cli/pull/#renames-and-migrations). > A green bare `test` means "statically clean, nothing was executed". It is not > a statement that the workflow runs. For that, pin it: ## With `--execution` / `--scenario`: the pinned run Runs the workflow **on your n8n instance** (MCP `test_workflow`) with external touchpoints pinned: the trigger, credentialed nodes, and HTTP Request nodes are fed captured data, while logic nodes (Code, Set, If, …) **execute for real** — on the instance's exact engine version, community nodes included, no Docker needed. The run targets the **draft** and is synchronous (the server caps it at 5 minutes; a timeout is reported as such). Afterwards each pure node's output is diffed client-side against the capture — divergence exits 1, so it's CI-gateable like the local-engine [`preflight --simulate`](/docs/cli/preflight/#the---simulate-stage). Pins come from the same sources that stage uses: a fetched capture (`--execution `) or a committed [scenario](/docs/cli/scenario/) (`--scenario `). **One of the two is required** — there is no fallback to "the newest capture lying around", because a bare `test` must never execute. A trigger/network node with no captured output **aborts before anything runs** — an unpinned one would hit the real world. So does a dangling `$('…')` reference: the static tier runs first, and a known-broken draft is never fired at the instance. `--trigger ` picks the start trigger in multi-trigger workflows. **Synthetic pins are the exception to the diff.** A `--scenario` with any `authored`/`scaffolded` node (see [provenance](/docs/cli/scenario/#provenance-and-synthetic-pins)) is reported "**synthetic pins — proves executability, not output correctness**": no per-node diff is asserted, and `ok` reflects only that the instance run succeeded. A capture-only run keeps the diff/exit-1 semantics above unchanged. `--json` adds `syntheticPins: boolean` and `provenance`. ## Where `test` sits: after the push ```sh n8n-decanter preflight # 1. is my local code sound? (local, changes nothing) n8n-decanter push # 2. make it the draft n8n-decanter test # 3. ← you are here n8n-decanter publish # 4. go live ``` `test_workflow` runs n8n's **draft**. Before step 2 the draft is not your code, so an instance run would grade something you aren't shipping — which is why [`preflight`](/docs/cli/preflight/) does **not** run `test` as a stage. Push first, then test what you pushed. | Command | Where it runs | What it needs | Reach for it when | | --- | --- | --- | --- | | [`preflight --offline`](/docs/cli/preflight/) | locally, static | nothing | every edit — layout + types, offline | | [`preflight --simulate`](/docs/cli/preflight/#the---simulate-stage) | local engine, runtime | Docker + a capture/scenario | runtime evidence about **local** code, before pushing; CI without an instance; enforced network isolation | | [**`preflight`**](/docs/cli/preflight/) | the above, scored | as available | **before `push`** — one verdict over your local code | | **`test`** (bare) | **your instance**, static | MCP | **after `push`** — is the draft internally sound? nothing runs | | **`test --execution/--scenario`** | **your instance**, runtime | MCP + a capture/scenario | **after `push`** — instance-exact engine, community nodes, no Docker | The split follows the same line as everything else here: `preflight` grades your **local files**, `test` grades **the instance's draft**. The static tier is not a second `preflight --offline` — it reads a different artifact. ## What gets tested — local code or the draft? `test_workflow` always runs the **draft tip**. When your local code differs from the draft: - **On a terminal**, `test` asks what you want to test: your **local code** (it pushes to the draft first — the same drift-guarded, draft-only push the `push` verb does; nothing is ever activated) or **what's on n8n now** (worded as "the live workflow" when draft and published version match, "the current n8n draft" when they diverge). On an unpublished workflow it skips the question and just pushes — updating a draft nobody runs is the obvious intent. After a pushed test you choose to **keep** the draft (then [publish](/docs/cli/publish/) when ready) or **restore** the pre-test draft — via n8n's version history (`restore_workflow_version`, n8n ≥ 2.29) with a byte-exact write-back fallback for older instances; the snapshot is persisted to a gitignored file first, so a crash can't lose it. - **Non-interactively** (piped, CI, agents), `test` **never mutates**: it tests the draft as-is and prints "tested the draft, not your local code — run `n8n-decanter push` first". There are no choice flags; the choices are verb composition (`push`, then `test`). Either way **the live (published) version is never affected** — the run and any push land on the draft only. Requirements: the MCP connection ([init](/docs/cli/init/)), the workflow's "Available in MCP" flag, an n8n new enough to ship `test_workflow` (~2.3x), and a workflow with a trigger node. `--json` emits the full report for scripts. --- # mcp connect Source: https://buttjer.github.io/n8n-decanter/docs/cli/mcp-connect/ ```sh n8n-decanter mcp connect ``` The **stdio MCP guard** — the default way a coding agent reaches your n8n instance's MCP server. You never run it by hand: the scaffolded `.mcp.json` (and `opencode.json`) from [init](/docs/cli/init/) already contains ```json { "mcpServers": { "n8n-instance": { "command": "npx", "args": ["--no-install", "n8n-decanter", "mcp", "connect"] } } } ``` and the agent spawns it per session. (It runs through `npx --no-install` so the command resolves whether decanter is installed **globally** or as a **local** project dependency — a bare `n8n-decanter` would only resolve on the agent's `PATH`, i.e. a global install, and fail silently otherwise. `--no-install` keeps it strictly local: it never downloads from npm, so a missing install fails loudly instead.) It speaks MCP over stdio to the agent and forwards each call to your instance's `/mcp-server/http` with **decanter's own credentials** (from `.env` / `.decanter-auth.json`) — the agent never holds an n8n credential, and because stdio pipes are private to the two processes, **no session secret exists at all**. The guard is the same one [mcp serve](/docs/cli/mcp-serve/) enforces over HTTP: - **Blocked:** `update_workflow` calls that write Code-node source (`jsCode`) — the caller gets an instructive tool error pointing at the file \+ [push](/docs/cli/push/) flow. - **Blocked:** `publish_workflow` when the draft it would take live carries a dangling `$('…')` reference — the same check [`test`](/docs/cli/test/) and [`publish`](/docs/cli/publish/) run, so an agent cannot go live around the verb. **Fail-closed**: if the check itself cannot run, the publish is refused too, and the message says the *check* failed rather than blaming the workflow. - **Everything else forwards untouched**: reads, structure edits (`addNode`, `renameNode`, wiring), archiving — the whole n8n MCP surface, SSE responses included. That combination is what powers the guarded authoring loop: an agent builds and wires structure over MCP (adding Code nodes **without** `jsCode` — the guard blocks code), then `pull` lands each new Code node as an empty file in `code/`, and the first `push` seeds its source from the repo. **Live mirror.** When the guard forwards a structure edit (a non-blocked `update_workflow`), it schedules a debounced background `pull` of that workflow, so the read-only `workflow.json` snapshot (+ code files + state) refreshes itself — the clean git diff of structure changes keeps pace with the agent, with no manual `pull`. It's fire-and-forget (never blocks the agent's next call), git-gated (a dirty tree is safety-committed before the pull; with no git it's skipped), and tracked-only (a brand-new, untracked workflow is left for an explicit `pull`). On by default; set `"liveMirror": false` in `decanter.config.json` to turn it off (CI / deterministic setups). Failure posture matches the HTTP guard: unparseable input is refused (**fail closed**), and an unreachable instance answers the agent with a JSON-RPC error naming the host instead of hanging. Logs go to stderr; stdout carries only protocol messages. The process ends when the agent closes the session. ## What the guard logs On stderr, so it never touches the protocol stream: ``` guard: connected to https://n8n.example.com — forwarding all n8n MCP tools, blocking jsCode writes in update_workflow guard: forwarded search_workflows guard: forwarded get_workflow_details blocked a jsCode write (update_workflow) — pointed the agent at the file + push flow ``` - **The startup line means the guard is alive.** Without it, an empty log is ambiguous — "ran and blocked nothing" and "never started" look identical, and they are opposites. If you see no startup line, the guard did not spawn; check the command in your `.mcp.json`. - **One line per forwarded tool call** — every n8n MCP call an agent makes goes through the guard, so this is the one place that answers *what did the agent do to my instance?* - **Tool names only, never arguments.** Arguments carry workflow content and pinned run data; keeping them out means the log is not a secret surface and is safe to attach to a bug report. `mcp serve` logs the identical lines — the two transports share this, the same way they share the guard rule itself. Prefer `mcp connect` wherever the agent's MCP config can spawn a command. For harnesses that only accept an MCP **URL**, use [mcp serve](/docs/cli/mcp-serve/) — the same guard as a localhost HTTP proxy with a per-session secret. --- # mcp serve Source: https://buttjer.github.io/n8n-decanter/docs/cli/mcp-serve/ ```sh n8n-decanter mcp serve [--port N] ``` Starts the **MCP guard-proxy**: a localhost HTTP endpoint that speaks n8n's MCP protocol and forwards everything to your instance's `/mcp-server/http` — with decanter as the **sole credential holder** and one rule enforced technically. It is the **HTTP variant** of the guard: for agents whose MCP config can spawn a command, prefer [mcp connect](/docs/cli/mcp-connect/) — the stdio form the scaffolded `.mcp.json` already wires, with no secret to manage. `mcp serve` exists for harnesses that only take an MCP **URL**: - **Blocked:** `update_workflow` calls that write Code-node source. That covers both routes n8n exposes: a `jsCode` **key** anywhere in the arguments (`updateNodeParameters`, `addNode`), and a `setNodeParameter` whose JSON-Pointer `path` targets `jsCode` (where the code rides a scalar `value` and no `jsCode` key appears). The caller gets an instructive tool error pointing at the file + [push](/docs/cli/push/) flow instead — Code-node source lives in this repo, not in ad-hoc MCP writes. The full op vocabulary is deliberately not enumerated; only the two source-writing routes are intercepted. - **Blocked:** `publish_workflow` when the draft it would take live carries a dangling `$('…')` reference — the same check [`test`](/docs/cli/test/) and [`publish`](/docs/cli/publish/) run. Without it the go-live gate is bypassable: an agent could publish over raw MCP and skip the verb entirely. **Fail-closed** — if the check itself cannot run (n8n unreachable), the publish is refused too, and the message says the *check* failed rather than blaming the workflow. - **Everything else passes through untouched**, including SSE responses: reads, structure edits, wiring, the n8n build/lifecycle tools. Like [mcp connect](/docs/cli/mcp-connect/), a forwarded structure edit also triggers the **live mirror** — a debounced background `pull` that refreshes the read-only `workflow.json` snapshot with no manual `pull` (fire-and-forget, git-gated, tracked-only; on by default, `"liveMirror": false` to disable). Point your agent's MCP config at the printed URL with the printed **session secret** as its `Authorization` header — the agent never sees an n8n credential, and the secret rotates on every `mcp serve` run. The current endpoint + secret also land in a gitignored `.decanter-proxy.json`, which the scaffolded `mcp-route-check.mjs` session hook uses to nudge agents whose MCP config still points at the instance directly. ```json { "mcpServers": { "n8n-instance": { "type": "http", "url": "http://127.0.0.1:5680/mcp-server/http", "headers": { "Authorization": "Bearer " } } } } ``` Safety properties: binds `127.0.0.1` only; unparseable request bodies are refused (**fail closed**), oversized bodies are capped; requests without the session secret never reach n8n. The blast radius of a proxy outage is availability, not integrity — decanter's own sync (`pull`/`push`/`watch`) never routes through the proxy. `--port` picks the listen port (default `5680`; `0` for an ephemeral one — note your agent config then changes every run). Stop with Ctrl-C. --- # executions Source: https://buttjer.github.io/n8n-decanter/docs/cli/executions/ ```sh n8n-decanter executions [workflow…] [--status=success|error|waiting] [--limit=N] n8n-decanter executions # fetch one execution by id n8n-decanter executions [workflow…] clean # delete fetched data (offline) ``` Fetches recent execution data — the full run JSON, newest first — for each workflow into `workflows//executions/.json`. Read-only against the API. The point is to see the **real items each node produced** and copy those shapes into [node run](/docs/cli/node-run/) fixtures, instead of guessing. A purely numeric argument is treated as an ``; everything else is a ``. Uses the n8n **public API key** (`N8N_API_KEY`) — execution data is one of the surfaces n8n's MCP server doesn't serve, so this verb keeps the REST path. Without a key it fails with guidance. ## Options | Flag | Meaning | | --- | --- | | `--status=success\|error\|waiting` | Only fetch executions in that state | | `--limit=N` | How many to fetch (default 5, API cap 250; `--limit N` also works) | A **numeric argument** is treated as a single execution id: it fetches just that execution and routes the file to its workflow's folder. ## Where the items live in the JSON Each node's output items are at: ```txt data.resultData.runData[""][0].data.main[0][] ``` That array is exactly the `items` a [node run](/docs/cli/node-run/) fixture feeds a node — copy a real shape in and your offline run matches production. ## Never commit run data Each `executions/` dir is written **self-ignored** (it contains a `.gitignore` of just `*`) because run data can hold credentials and PII — it must never reach git. `init`'s scaffolded root `.gitignore` also lists `workflows/*/executions/`. ## `executions clean` Offline. Deletes the fetched `executions/` dirs for the given workflow refs, or for every pulled workflow when no ref is given. Run it when you're done. ## Caveat: published version Executions run the **published** workflow version (n8n 2.x), not necessarily your local draft — so treat the data as convenience reference, not ground truth about your current code. To make that concrete, `executions` **warns** when a fetched execution ran a published version different from your local draft (comparing the execution's `workflowVersionId` against `workflow.json`'s `versionId`): ```txt ! captured executions ran published version ; your draft is — the data may not match the code you're editing ``` The files are still written — it's a warning, not an error — but it tells you the captured shapes may be a step behind the code in front of you. --- # data-tables Source: https://buttjer.github.io/n8n-decanter/docs/cli/data-tables/ ```sh n8n-decanter data-tables [table…] # schema + rows for every table (or the named ones) n8n-decanter data-tables [table…] --filter '' --search --sort --limit N --all n8n-decanter data-tables [table…] clean # delete fetched data (offline) ``` Fetches each n8n **data table** (the built-in project-scoped tables, n8n ≥ 2.x) — its schema and its rows — into local files, so you can develop and debug a workflow against the **real table contents** offline (e.g. to give a [node run](/docs/cli/node-run/) fixture realistic shapes, or just to eyeball what a table holds). **Read-only against the API** — the CLI never creates, updates, or deletes a data table, column, or row. A `` is a table's **id or its exact name** (case-insensitive). With no argument, every table is fetched. Uses the n8n **public API key** (`N8N_API_KEY`) — MCP's data-table tools are add-only with no row reads, so this verb keeps the REST path. Without a key it fails with guidance. ## Where the data lands Data tables are **project-scoped — not owned by a workflow** — so, unlike [executions](/docs/cli/executions/) (which nest under each workflow folder), they land in a **single top-level `data-tables/` dir** next to `decanter.config.json`: ```txt data-tables/ / meta.json # id, name, projectId, fetchedAt, rowCount + the applied filter/search/sort/limit columns.json # the table schema (each column's name + type) rows.json # the (possibly filtered) rows ``` The slug is the kebab of the table name with its id appended (names aren't guaranteed unique). `meta.json` records what produced `rows.json`, so a filtered slice is self-describing and never mistaken for the whole table. ## Pull a filtered slice A table's rows can be large, so the verb pulls a **slice** by pushing the filter down to the server rather than downloading everything: | Flag | Meaning | | --- | --- | | `--filter ''` | Server-side condition filter — a JSON string passed 1:1 to the API (see shape below) | | `--search ` | Free-text search across string columns | | `--sort ` | Sort by a column, ascending or descending | | `--limit N` | Rows per page (default 100, API cap 250; `--limit N` or `--limit=N`) | | `--all` | Follow the cursor to exhaust the (usually filtered) result, not just one page | The `--filter` value is n8n's own condition object as a JSON string: ```json { "type": "and", "filters": [ { "columnName": "status", "condition": "eq", "value": "active" } ] } ``` `condition` is n8n's row operator (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `like`, …); combine several under `"type": "and"` / `"or"`. For example, only the active orders, newest first: ```sh n8n-decanter data-tables "Orders" \ --filter '{"type":"and","filters":[{"columnName":"status","condition":"eq","value":"active"}]}' \ --sort createdAt:desc ``` The applied filter, search, sort, and limit are written into that table's `meta.json` alongside the resulting `rowCount`. ## Never commit table data Each `data-tables/` dir is written **self-ignored** (it contains a `.gitignore` of just `*`) because table rows can hold PII — they must never reach git. `init`'s scaffolded root `.gitignore` also lists `data-tables/`. ## `data-tables clean` Offline. Deletes the whole local `data-tables/` dir. Run it when you're done. ## Config gate The fetch is gated by the `dataTables` key in [`decanter.config.json`](/docs/concepts/configuration/) (default `true`). Set it to `false` to disable the fetch entirely — the verb then refuses with a clear message, and the recommended API key needn't carry the data-table read scopes. `data-tables clean` stays available regardless. ## Scopes While `dataTables` is on, the [recommended scoped key](/docs/concepts/configuration/) needs the read scopes `dataTable:list`, `dataTable:read`, `dataTableColumn:read`, and `dataTableRow:read` (a full-access key also works). Data-table endpoints need n8n **≥ 2.x** — on an older instance the fetch reports that it isn't available. --- # scenario Source: https://buttjer.github.io/n8n-decanter/docs/cli/scenario/ ```sh n8n-decanter scenario create [""] [--execution ] [--scaffold] [--json] n8n-decanter scenario create "" --extend n8n-decanter scenario check [""] [--json] ``` Both take a workflow ref first. Leaving it off **on a terminal** opens the [picker](/docs/cli/overview/#interactive-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](/docs/cli/test/) (on your instance) and [`preflight --simulate`](/docs/cli/preflight/#the---simulate-stage) (on a local engine), run and diff against. It's the **only committed pin artifact**: `workflows//scenarios/.json` is a self-contained, execution-shaped file, so `preflight --simulate --scenario ` / `test --scenario ` 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: ```sh 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: ```sh 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. - **``** 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 `** seeds the scenario from a captured execution (`executions/.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`: ```jsonc { "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[\"\"] = [ { \"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: ```sh 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: ```txt 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[""] = [ { "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: ```jsonc "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 ```sh 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`. ## 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 ` / `test --scenario `) — 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/.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](/docs/cli/preflight/#what-the-compliance-guard-catches) (so from `push`, `watch`, and `preflight` alike), naming the replacement: recreate the data as a scenario (`scenario create --execution `), 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. --- # list Source: https://buttjer.github.io/n8n-decanter/docs/cli/list/ ```sh n8n-decanter list [--remote] [--json] ``` Lists every pulled workflow with its name, id, and folder path. Offline by default. ## `--remote` Also queries the instance (MCP `search_workflows` — it sees every workflow) and appends the ones that haven't been pulled yet: `(not pulled)` when they are ready to pull, `(not available in MCP)` when the workflow still needs its "Available in MCP" flag in n8n — with a hint to where the switch lives. The quick way to find the id for a [pull](/docs/cli/pull/), though `pull` also resolves unpulled names directly. `--json` emits rows for tooling; remote-only rows carry `dir: null` and an `mcpAvailable` boolean. --- # backup Source: https://buttjer.github.io/n8n-decanter/docs/cli/backup/ ```sh n8n-decanter backup create # instance -> git n8n-decanter backup restore [] # git -> a NEW workflow n8n-decanter backup list # retained backups (offline) ``` A **git-native, redeployable disaster-recovery store** for a workflow. Both n8n's MCP server and its REST API only expose the current **draft tip**, and MCP's read is sanitized (no credential refs, no `pinData`/`staticData`/ `description`) — so **git is the only place a redeployable version history can live**. `backup` captures the workflow's full REST export into a committed `backups/` folder and can redeploy it onto a rebuilt or fresh n8n. It's a second version+recovery layer *outside* n8n, one that survives the instance being lost. This is **disaster recovery, not sync**: `restore` creates a *new* workflow, it never reconciles an existing one. Workflow structure stays n8n's to own (the daily loop is [pull](/docs/cli/pull/) / [push](/docs/cli/push/) over MCP). Uses the n8n **public API key** (`N8N_API_KEY`) — the full-fidelity workflow `GET`/`POST` is a surface MCP can't serve. Without a key, `create`/`restore` fail with guidance (`list` is offline and needs none). ## The store ```txt workflows/order-sync/ backups/ 2026-07-23T14-30-00Z.8dd14331.json # each `backup create` appends one 2026-07-24T09-15-00Z.2f3335b8.json ``` Each file is the full REST export (`GET /workflows/:id`) with: - **`jsCode` kept as a `//@file:` placeholder** — the Code-node source is never duplicated; `restore` re-inlines it from the folder's `code/` files (`.ts` compiled). - **`pinData` + `staticData` stripped** — runtime state, churny and semi-sensitive. - **credential refs + `description` kept** — the "which credential" rebind hint for a restore onto another instance. The filename is a filesystem-safe timestamp plus the short `versionId`; the full `versionId` lives inside. ## `backup create` Reads the current draft over REST and writes a new timestamped file. It - **skips when `versionId` is unchanged** since the latest backup (no redundant identical copies), and - **rolling-prunes** the working set to `backupLimit` (config, default **20**; `0` keeps all — git still holds the full history regardless). The file is a **full export** — it carries credential refs and any secrets embedded in node parameters — so it is **not auto-committed**. `create` prints a warning; review the file and `git add` it deliberately. (The store is *not* self-gitignored, unlike [`executions/`](/docs/cli/executions/) — committing it is the whole point.) ## `backup restore` Selects a backup — the **latest** by default, or the one named by the optional `` argument, or a chooser on a terminal — assembles the full JSON (structure + credential refs, each Code node's `jsCode` re-inlined from its `code/` file), runs it through the compliance guard, and REST-`POST`s it as a **new workflow**: - a **new workflow id**, but **node ids are preserved** (the REST `GET → POST` round-trip is lossless), and - it lands **unpublished** — `restore` prints the credential-rebind hints (the refs point at the *source* instance; recreate/rebind them on the target) and the editor URL; **publish** is your next step. ### The `` argument A **backup ref**, resolved by shape the way a `` ref is — no flag, and no need to say which kind you have. Paste either column [`backup list`](#backup-list) prints: | You pass | Matches | | --- | --- | | `2026-07-24T09-15-02Z` | that timestamp exactly | | `2026-07-24` | the first backup whose filename starts with it (a bare date is enough) | | `a1b2c3d4` | the short `versionId` in the filename | | `a1b2c3d4-…-full-uuid` | the full `versionId`, as pasted from n8n | ```sh n8n-decanter backup restore order-sync # the latest (or the chooser, on a terminal) n8n-decanter backup restore order-sync 2026-07-24 # the first backup from that day n8n-decanter backup restore order-sync a1b2c3d4 # that exact version ``` A ref that matches nothing is an **error** — restore never quietly falls back to the latest. ## `backup list` Offline. Prints the retained backups — timestamp · `versionId` · node count. `--json` emits the same as a machine-readable array. ## Not a backup of everything `backup` captures one workflow's structure + Code. It does **not** back up credentials themselves (only the refs), data tables, or execution history — recreate credentials on the target and rebind them after a restore. --- # node run Source: https://buttjer.github.io/n8n-decanter/docs/cli/node-run/ ```sh n8n-decanter node run [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`](/docs/cli/test/) (which runs the real n8n draft over MCP) instead of guessing. See [the boundary](#whats-emulated-vs-unsupported) 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`](/docs/cli/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`](/docs/cli/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: ```json { "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 } } ``` - `input` feeds `$input`/`$json`; without a fixture the input defaults to a single empty item. - `nodes` backs `$('Node Name')`, `$node['Node Name']`, and `$items('Node Name')`. - `params` backs `$input.params` (defaults to `{}`). - `env` backs `$env`. Like n8n's own scoped `$env`, it is **empty by default** — set it explicitly with this field, or pass **`--allow-env`** to inherit the CLI process's environment (which may include `N8N_API_KEY` and other secrets), so a node that prints `$env` never leaks the host environment by accident. - `vars` / `secrets` back `$vars` / `$secrets`. These are **instance-scoped** — without a fixture value `run` can't know them, so any access throws the friendly *"not emulated in `run` — use `test`, or pin `vars`/`secrets`"* message. Pin them here to run a node that reads them offline. - `$getWorkflowStaticData` is seeded from `workflow.json`'s `staticData` (`global` and this node's slice); a fixture `staticData` replaces the matching slice (`"node"` refers to the node being run). Mutations are visible during the run but never persisted — `run` is offline. - `workflow`, `execution`, and `prevNode` back `$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](/docs/cli/executions/) and copy a node's items into your fixture. --- # completion Source: https://buttjer.github.io/n8n-decanter/docs/cli/completion/ ```sh eval "$(n8n-decanter completion zsh)" # append to ~/.zshrc (after compinit) eval "$(n8n-decanter completion bash)" # append to ~/.bashrc ``` Prints a completion script for your shell. Completion covers verbs, flags, and the names/ids of pulled workflows; candidates are computed at completion time, so they stay current without regenerating the script. Offline and silent when no config is in reach. --- # Sync layout & data model Source: https://buttjer.github.io/n8n-decanter/docs/concepts/sync-layout/ Each synced workflow is one folder under the configured root: ``` workflows/ order-sync/ # kebab-case slug of the workflow name (a stable local pick) workflow.json # read-only structure snapshot; code replaced by placeholders .decanter.json # sync state — commit it, never edit it code/ parse-order.js # one file per Code node, kebab-case-named amazon-feed.ts scenarios/ happy-path.json # committed pin-data set (see below) ``` The split of responsibilities (since the MCP-native sync): **Code-node source lives here, in git, as the files decanter syncs. Workflow structure lives in n8n** — you change it in the editor or over n8n's MCP tools (reached through decanter's guarded proxy), and decanter mirrors it into the read-only snapshot on every pull. ## Folder names A **new** workflow's folder is the **kebab-case slug** of its name (`"Order Sync"` → `order-sync/`). If that slug is already taken by a different workflow, it falls back to `-` (the same collision suffix node files use) and warns. Folders are a **stable local pick**: an existing folder is *never* renamed, no matter who renames the workflow (the n8n UI or an agent over MCP). The always-current display name lives in `.decanter.json` (`name`) instead, so the picker, [list](/docs/cli/list/), and ref-resolution stay accurate while your working directory and git history never churn. Any folder name still resolves as a ref, so a hand-rename works too. (Folders synced before this change keep their original names and keep working.) ## `workflow.json` — the read-only structure snapshot The workflow's structure, pretty-printed with a stable key order — except each Code node's `jsCode`, whose entire value is a placeholder pointing at the node's source file: ```json "parameters": { "mode": "runOnceForAllItems", "jsCode": "//@file:code/parse-order.js" } ``` `workflow.json` never contains code, and **nothing pushes it**: pull refreshes it (reading the workflow *tip* — the draft when one exists), review diffs and the offline tooling ([`preflight --offline`](/docs/cli/preflight/), [node run](/docs/cli/node-run/), the local-engine replay `preflight --offline --simulate`) read it, and local edits to it change nothing in n8n. When the structure changed remotely, preflight's `snapshot` check warns that the snapshot is out of date and `pull` refreshes the file. The one meaningful local edit: **re-pointing a `//@file:` placeholder** (for a `.js` ↔ `.ts` conversion) — the placeholders are the human-visible file map, and push honors a re-point. Viewer-relative and derived fields are stripped on pull (`shared`, `scopes`, `canExecute`, the published-version copy `activeVersion`, and the published-version pointer `activeVersionId` — state that churns on each publish, with no local reader; [preflight](/docs/cli/preflight/)'s `lifecycle` check reads `activeVersionId` off the live workflow). The draft `versionId` is kept, since the [executions](/docs/cli/executions/) stale-capture warning compares against it. ## `code/` Node sources, named in kebab-case after their node (`Parse Order` → `code/parse-order.js`). `.js` files are lossless (byte-identical round-trip); `.ts` files are one-way — see [TypeScript nodes](/docs/concepts/typescript-nodes/). Layouts from older versions (files at the folder root) migrate automatically on the next pull. ## `scenarios/` Committed, full-workflow **pin-data sets** — `scenarios/.json`, each a self-contained, execution-shaped file captured from a real run or scaffolded from the workflow's schemas. [`test`](/docs/cli/test/) and [`preflight --simulate`](/docs/cli/preflight/) replay one with `--scenario ` and diff each node against it. Unlike the gitignored `executions/` sibling (temporary capture data), **`scenarios/` is tracked in git**, so a scenario-based replay is reproducible for teammates and CI. See [scenario](/docs/cli/scenario/) for how they're created, filled, and validated. ## `.decanter.json` Per-folder machine state: the node-id → file map (with per-node cached names), the per-node sync hashes used by the [drift guard](/docs/concepts/push-gates/), and the cached workflow **`name`** (the display name, refreshed on every pull — it's why a kebab folder still reads as the workflow, and why `list`/the picker keep working even if `workflow.json` is missing or corrupt). Node **ids** are the identity anchor — they survive renames made anywhere (the n8n UI, or any agent over MCP), so a rename just moves the local file on the next pull. Commit it; never edit it by hand or "fix" a hash. ## `.decanter-auth.json` (sync-dir root) Not per-workflow — the MCP OAuth credentials [init](/docs/cli/init/) minted (client id + refresh token, rotated automatically). Gitignored, machine-owned; delete it and re-run `init` to re-consent. ## `.decanter-template.json` (sync-dir root) Not per-workflow — one file at the sync-dir root recording the hash of every template file as [init](/docs/cli/init/) copied it. It's the baseline that makes re-running `init` modification-aware (refresh untouched files, leave your edits, report drift). Commit it; never edit it by hand. ## Auto-commits After every successful push **and** pull, the workflow's folder is git-committed automatically (scoped to that folder; outside a git repo it just warns). `"commitOnPush": false` / `"commitOnPull": false` in the [config](/docs/concepts/configuration/) turn it off. --- # TypeScript nodes & bundling Source: https://buttjer.github.io/n8n-decanter/docs/concepts/typescript-nodes/ Both tiers run as a Code-node **function body** — top-level `return` required, and the same typed n8n globals (`$input`, `$('…')`, `DateTime`, …) are available. ## `.js` nodes — the lossless default What you write is byte-for-byte what runs in n8n and what round-trips back on pull. Type safety via JSDoc (`// @ts-check` on the first line, `@typedef` for shapes). **No imports** — a `.js` node is pushed verbatim into n8n, where Code nodes cannot load modules; the layout guard rejects them (at push time, and as [preflight](/docs/cli/preflight/)'s `layout` check). Comments survive into n8n and document the node in place. ## `.ts` nodes — one-way Choose `.ts` when the type surface is heavy (interfaces, generics, discriminated unions). The local `.ts` is the only source of truth: - [push](/docs/cli/push/) compiles it with esbuild and appends a `// @ts-n8n sha256:…` marker line to the uploaded code — the marker is how pull recognizes a TS-managed node. Never write that marker yourself. - **Comments are stripped and line numbers shift** in the compiled output — n8n error line numbers won't match the source, and the node code shown in the n8n UI is undocumented output. Documentation belongs in the `.ts`. - [pull](/docs/cli/pull/) never touches `.ts` sources; instance-side edits are warned about — inspect them with [diff](/docs/cli/diff/) and port what you want to keep into the `.ts` by hand (the next push overwrites the remote edit). To convert a node, replace `code/.js` with `code/.ts` and change its `//@file:` placeholder in `workflow.json` — the tool picks up the new extension on the next push. A [pull](/docs/cli/pull/) in between (for example the live-mirror refresh after a structure edit) keeps your re-pointed `.ts` and won't revert the placeholder. The reverse works the same way: replace the `.ts` with a `code/.js` (plain JavaScript — the file is pushed verbatim) and re-point the placeholder. The next push clears the remote `@ts-n8n` marker even when the code is otherwise identical, so the node stops being TS-managed. **Push before you pull again**: until that push lands, a pull still sees the remote marker and treats the node as TS-managed (renaming the file back to `.ts`). ## Shared code and npm packages `.ts` nodes can import from `shared/*.ts` (values *and* types) and from npm packages installed in the sync dir and opted in via `"bundleDependencies"` in the [config](/docs/concepts/configuration/): ```ts import { total, type OrderLine } from "../../shared/money"; const lines: OrderLine[] = $input.all().map((i) => i.json as OrderLine); return [{ json: { total: total(lines) } }]; ``` Push bundles the imports into the compiled node, so the pushed code is **self-contained and runs anywhere — n8n Cloud included**, no `NODE_FUNCTION_ALLOW_*` setup. Each importing node carries its own copy, so keep helpers small; editing one shared file makes **every** importing node differ from the draft — [diff](/docs/cli/diff/) compiles before comparing, so it lists them all, and [preflight](/docs/cli/preflight/)'s `parity` check counts them. Rules: imports at the top of the file only; relative paths must stay inside the repo; pure-JS packages only — unlisted npm packages and Node builtins (`node:*`, `fs`, `crypto`, …) are compile errors; never `require()`. `.js` nodes stay import-free — that tier is byte-lossless by contract. --- # Push gates Source: https://buttjer.github.io/n8n-decanter/docs/concepts/push-gates/ [push](/docs/cli/push/) runs three independent checks, in order. Only the last one is bypassed by `--force`. ## 1. Typecheck gate The same wrapper-based typecheck as [preflight](/docs/cli/preflight/)'s `types` check — see [Type checking](/docs/concepts/type-checking/). Blocking; skip with `--no-typecheck` (auto-skipped when no `tsconfig.json` is found). ## 2. Compliance guard Layout violations are **hard errors that `--force` does not bypass** — they would corrupt sync state. The full list is on the preflight page, under [what the compliance guard catches](/docs/cli/preflight/#what-the-compliance-guard-catches): placeholder integrity, connection integrity, duplicate names/ids, orphan files, dangling `$('…')` references, marker misuse, and a leftover retired `fixtures/` dir. Standalone: `n8n-decanter preflight --offline` runs this guard plus the typecheck and nothing else — no credentials, no network, and every violation listed under the failing `layout` check. The guard also **warns without blocking** about an inline Python `pythonCode` node and a committed scenario that embeds inline Code-node source under `workflowData`. ## 3. Per-node drift guard If a Code node's **remote code** changed since the last sync (and differs from what you're about to push), push aborts with `pull first`. This is the only gate `--force` bypasses — it exists so you don't silently clobber code edited on the instance. Remote **structure** changes never block a push: pushes write only `jsCode`, and the structure snapshot is mirrored (read-only), never pushed from here. The interplay with pull matters: **pulling records the remote code as the new sync base**, so after a warned pull the next push overwrites the surfaced remote edits by design — [diff](/docs/cli/diff/) and git history are the safety net. On the gate side the same situation shows up as preflight's `drift` check: a node changed both locally and remotely fails it as a `CONFLICT`. Per-node sync hashes are stored in [`.decanter.json`](/docs/concepts/sync-layout/); "last synced" means the last push *or* pull. A remote edit that happens to match your local code re-baselines silently instead of aborting. --- # Configuration Source: https://buttjer.github.io/n8n-decanter/docs/concepts/configuration/ `decanter.config.json` is searched upward from the current directory; credentials come from `.env` / `.decanter-auth.json` next to it or from the environment. ```json { "root": "./workflows", "workflows": ["0cXNQKKzmO0pXiCq"], "commitOnPush": true, "commitOnPull": true, "requestTimeoutMs": 30000, "n8nVersion": "2.31.4", "dataTables": true, "liveMirror": true, "backupLimit": 20, "bundleDependencies": ["zod"] } ``` | Key | Default | Meaning | | --- | --- | --- | | `root` | — | Directory holding the workflow folders. | | `workflows` | `[]` | Workflow ids processed when a command gets no refs. | | `commitOnPush` | `true` | Auto-commit the workflow folder after a successful push. | | `commitOnPull` | `true` | Same for pull. | | `requestTimeoutMs` | `30000` | Request timeout (MCP and API) — raise for slow instances. | | `n8nVersion` | unset | n8n version the local engine behind [`preflight --simulate`](/docs/cli/preflight/) pins to (e.g. `"2.31.4"`); `--n8n-version` overrides it per run. Unset falls back to the project's default with a hint. | | `dataTables` | `true` | Whether the read-only [data-tables](/docs/cli/data-tables/) fetch is available. `false` refuses it (and the API key needn't carry the data-table read scopes); `data-tables clean` still works. | | `liveMirror` | `true` | Refresh the read-only `workflow.json` snapshot in the background after an agent restructures a workflow through the [guard](/docs/cli/mcp-connect/) (a forwarded `update_workflow`). `false` disables the auto-refresh (CI / deterministic setups). | | `backupLimit` | `20` | Cap on the retained [`backups/`](/docs/cli/backup/) working set per workflow. Each `backup create` rolling-prunes the oldest beyond this; `0` keeps all (git holds the full history regardless). | | `bundleDependencies` | `[]` | npm packages `.ts` nodes may import; [bundled on push](/docs/concepts/typescript-nodes/). Pure-JS only. | ## Credentials The sync rides n8n's **MCP server**; the public API key is an optional extra. In order of resolution: 1. **`N8N_HOST`** — always required for online verbs (`.env` or environment). 2. **MCP credentials** (the sync verbs — pull, push, diff, watch, publish, unpublish, test, and `preflight` without `--offline` — plus the `mcp connect`/`mcp serve` guard): - `N8N_MCP_TOKEN` (`.env` or environment) — a rotatable token from n8n → Settings → MCP → API key. Takes precedence when set. - Otherwise `.decanter-auth.json` — the OAuth client id + refresh token [init](/docs/cli/init/) minted via browser consent. The refresh token rotates on every use; the file is rewritten automatically. Delete it and re-run `init` to re-consent (also the fix for a "MCP session expired" error). 3. **`N8N_API_KEY` (optional)** — only for the verbs MCP cannot serve: [executions](/docs/cli/executions/), [data-tables](/docs/cli/data-tables/), and [backup](/docs/cli/backup/). Scope it minimally: `execution:read`, `execution:list`, `workflow:list` (init's connection check), the `dataTable:*` read scopes (only while `dataTables` is on), and `workflow:read` + `workflow:create` (only for `backup` create/restore's full-fidelity GET/POST). The instance needs **MCP access enabled** once (n8n → Settings → MCP; requires an n8n with the built-in MCP server, ~2.20+), and each synced workflow needs its **"Available in MCP"** flag (workflow card ⋯ menu, or workflow settings) — [list --remote](/docs/cli/list/) and the picker show which workflows still need it. `preflight --offline`, `node run`, `scenario check`, and plain `list` need no credentials at all (`scenario create --scaffold` is the exception — it needs MCP). --- # Type checking Source: https://buttjer.github.io/n8n-decanter/docs/concepts/type-checking/ n8n Code-node source is a *function body* — top-level `return`/`await` — which plain `tsc` rejects in `.ts` files (TS1108). The typecheck behind [preflight](/docs/cli/preflight/)'s `types` check and the [push gate](/docs/concepts/push-gates/) therefore wraps node files in an `async function` **in memory** and maps diagnostics back to real line numbers. A `.decanter.json` next to a file — or in the parent of its `code/` dir — is what marks it as a node file. Files on disk stay verbatim: never "fix" a node file by wrapping it in a function or stripping its top-level return. ## Editor false positives The IDE's own tsserver doesn't apply the wrapper, so editors show a spurious TS1108/TS1375/TS1378 on top-level `return`/`await` in node files. Ignore it — `n8n-decanter preflight --offline` is authoritative. Scaffolded sync dirs ship a TypeScript language-service plugin (`decanter-ts-plugin/`) that suppresses exactly these three codes on node files (all other diagnostics stay live). It activates once tsserver runs the workspace TypeScript: `npm install`, then in VS Code accept *Use Workspace Version* (offered via `.vscode/settings.json`); JetBrains uses the project TypeScript by default. ## Two tsconfigs in a sync dir The scaffolded `tsconfig.json` belongs to the workflow node files (with `n8n-globals.d.ts` typing `$input`, `$('…')`, `DateTime`, …). Its name is load-bearing — the typecheck discovers it by name, searching upward. Keep it where the config is. --- # Working with coding agents Source: https://buttjer.github.io/n8n-decanter/docs/agents/overview/ n8n-decanter is built to let AI coding agents work on workflows safely. A scaffolded sync dir ([init](/docs/cli/init/)) contains everything an agent needs to behave: - **`AGENTS.md`** — the tool-agnostic contract for the repo: how code is stored here (placeholders, `code/`, markers), the file-ownership rules, the rename checklist, and how to verify changes. Codex and opencode read it natively; Claude Code reads it through a one-line import in `CLAUDE.md`. - **Per-agent configs** — Claude Code, Cursor, Codex, opencode — kept as thin pointers to `AGENTS.md`, so every agent follows the same rules. - **Guard hooks** — on Claude Code and opencode, edits that would break a hard invariant are blocked *before the write happens*; a Claude Code PostToolUse hook runs [`preflight --offline`](/docs/cli/preflight/) after node edits. A second PostToolUse hook watches MCP `update_workflow` calls and speaks up when a `renameNode` leaves `$('Old Name')` references behind — n8n's rename rewrites the node name and connections only, so those refs are the caller's to repair (see [`pull`](/docs/cli/pull/)). It scans for the old name rather than running `preflight`, because it fires before the background snapshot refresh, while every reference still resolves. The same rules are enforced by the CLI at push time regardless of who made the edit. On Claude Code these live in **`.claude/settings.json`** — *project* scope, meant to be committed, so everyone who clones the repo gets the same permissions and hooks. `.claude/settings.local.json` stays yours for machine-specific rules: permission lists merge across the two and a `deny` beats an `allow`, so your local file can add to the policy but cannot unblock what the project denies. ## The hard invariants Violating these corrupts sync state, which is why they're machine-enforced: 1. `jsCode` in `workflow.json` never contains code — only `//@file:` placeholders. 2. Never write a `// @ts-n8n sha256:…` marker line — the tool appends it to compiled output on push. 3. `.decanter.json` is machine state — never edit it, never "fix" a hash. Two boundary rules sit next to them: **Code-node source is authored as files here and synced by decanter — never edited on the instance** (not in the UI, not via n8n's MCP tools or skills); and **`workflow.json` is a read-only snapshot** — structure changes go through n8n. n8n-decanter is built to pair with n8n's official skills pack: see [Using n8n's official skills](/docs/agents/n8n-skills/) for how the MCP guard (`mcp connect`; `mcp serve` for URL-only harnesses) makes that boundary safe by construction. ## Who runs what | Commands | Agent policy | | --- | --- | | `preflight --offline`, `node run`, `scenario` | Offline and safe — run freely (`scenario create --scaffold` is the exception; it needs MCP). Adding `--simulate` stays credential-free but boots a local Docker engine — minutes, not milliseconds, so opt in deliberately. | | `preflight`, `diff`, `list --remote` | Read the remote, no writes — safe, but they do contact the instance. `preflight` is the gate (exit 1 when `not ready`); `diff` is the view and **always exits 0**. | | `pull`, `push`, `watch` | Sync code with the instance. A push lands on the **draft** and never changes what is running, so it is **part of finishing the work** — code that only exists in the folder is not done. Say a word first if the workflow is published/active or a teammate is editing it. | | `publish`, `unpublish`, `push --publish` | **Change what is actually live — only when the user explicitly asks.** Never fold going live into "finishing the work". | | Structure/lifecycle acts over n8n's MCP (create, add/wire nodes — via the [guard](/docs/cli/mcp-connect/)) | Building the structure a request describes is part of the work. **Renaming or archiving something that already exists is not** — ask first. After a structure act, `pull` reconciles the local mirror. | | `test` | Grades the workflow's **draft** on the instance. **Bare** it is a static check — dangling `$('…')` references, nothing executes, no capture needed. **With `--execution`/`--scenario`** it executes the draft (pinned trigger/network nodes, real logic nodes); the live version is never affected and non-interactive runs never write. Either way it is only meaningful **after a `push`** — before one, the draft holds the old code (or nothing). It is the post-push check in `preflight → push → test → publish`, and the static half is what `publish` refuses on. | | Archiving (MCP `archive_workflow`) | **Outward-facing** — the workflow leaves the active list; a published one goes offline. Reversible only in the n8n UI. Never without an explicit instruction to archive *that* workflow. | | `push --force` | Never without explicit instruction — it overrides the per-node drift guard protecting code edited on the instance. | The default loop for an agent: edit → verify ([`preflight`](/docs/cli/preflight/), or `preflight --offline` to stay credential-free) → **push** → **`test`** (the draft now holds your code) → say what landed and what the test showed. Stop before `publish` unless the user asked for it. See [The offline feedback loop](/docs/agents/offline-loop/). --- # The offline feedback loop Source: https://buttjer.github.io/n8n-decanter/docs/agents/offline-loop/ Several verbs are fully offline — no credentials, no network, no live n8n — which makes them safe for agents to run without supervision: - **[`preflight --offline`](/docs/cli/preflight/)** — the static tier on its own: the layout-compliance guard (`layout`) plus the typecheck (`types`, the same wrapper that maps top-level-`return` node bodies back to real line numbers). `--offline` drops the instance tier, so nothing is read from n8n. Run it after editing any code file; treat a `not ready` verdict as a blocker. Every violation is listed in the failing check's indented `details` — the one-line message is only the summary. - **[`node run`](/docs/cli/node-run/)** — executes a node's body against a faked n8n context and prints the items it returns. With a fixture, `$input`, `$('Node Name')`, env, and static data are all controllable — real execution feedback without touching the instance. Adding `--simulate` (`preflight --offline --simulate`) keeps the loop credential-free and still never contacts n8n, but it boots a throwaway local engine to really *run* the workflow — Docker, and minutes rather than milliseconds. An occasional deeper pass, not the per-edit one. A typical agent iteration: ```sh # after editing code/parse-order.ts and workflow.json n8n-decanter node run workflows/order-sync/code/parse-order.ts fixture.json n8n-decanter preflight --offline # both green -> push: the draft is where the work lands, and code that only # exists in this folder is not done n8n-decanter push order-sync # now the draft holds your code -> grade it on the instance. This step leaves # the offline loop: `test` grades the DRAFT on n8n, so it only means anything # once you have pushed (before a push it would grade the old code). Bare, it is # a static check and executes nothing; add --scenario/--execution for a real run. n8n-decanter test order-sync # going LIVE (`publish` / `push --publish`) stays the user's call ``` Adding a Code node from scratch is a structure act — it happens **in n8n** (the editor, or an `addNode` MCP op through the [guard](/docs/cli/mcp-connect/) with **no** `jsCode`), then [`pull`](/docs/cli/pull/) lands it as an empty `code/` file with its placeholder and state entry (the node lands disconnected; wire it in n8n). Write the code in the file, verify with `node run` + `preflight --offline`, and the first push seeds the node's source. The [sync layout](/docs/concepts/sync-layout/) page shows the shapes. Because verification routes through the CLI, `n8n-decanter` must be on the sync dir's PATH — see [Installation](/docs/getting-started/installation/). ## Exit codes: one gate, one view `preflight` is the **gate** — exit `1` when the verdict is `not ready` (any check failed), `0` otherwise; `--fail-on=warn` makes a `caution` fail too. That is the exit code to branch on, offline or online. [`diff`](/docs/cli/diff/) is the **view** — the per-node line diff between your files and the n8n draft — and it **always exits `0`**, like `git diff`. Never read a clean `diff` exit as a passing check. (It is also not offline: it reads the draft from the instance.) --- # Using n8n's official skills Source: https://buttjer.github.io/n8n-decanter/docs/agents/n8n-skills/ **Use the [official n8n skills](https://github.com/n8n-io/skills).** They are the best way to give your coding agent real n8n expertise, and n8n-decanter is built to pair with them, not to replace them. It rides n8n's own MCP server and skills for everything about workflow *structure and lifecycle*, and owns just one layer on top: the **Code-node source**. The skills fill in the knowledge decanter deliberately doesn't duplicate. The one thing to know is the **boundary** — and decanter enforces it for you technically, so pairing the two is safe by construction. This page explains the integration, then how to turn it on. ## What the skills are `n8n-io/skills` is n8n's first-party agent knowledge pack: capability skills (markdown + inline examples) plus a routing meta-skill and reference docs, installed as a **plugin** (not an npm dependency). They come in two kinds, and the split matters here: - **Knowledge skills — lean on these freely.** Conceptual, standalone guidance for the runtime your Code-node files execute in. They document the same n8n your `.js`/`.ts` nodes run against, with **no instance mutation**: - `n8n-code-nodes-official` - `n8n-expressions-official` - `n8n-loops-official` - `n8n-error-handling-official` - `n8n-credentials-and-security-official` - `n8n-binary-and-data-official` - `n8n-data-tables-official` - `n8n-debugging-official` - **Build / lifecycle skills — the *default* path for structure.** `n8n-workflow-lifecycle-official`, `n8n-node-configuration-official`, `n8n-subworkflows-official`, `n8n-agents-official`, `n8n-extending-mcp-official`. These drive the n8n MCP server to create, build, wire, rename, publish, and archive workflows — which, under n8n-decanter, is exactly how structure work happens (decanter has no structure verbs of its own). The single carve-out is below. The plugin installs the **whole pack** (you can't cherry-pick), and it isn't aware of this repo's layout. That's fine — the MCP guard plus the scaffolded `AGENTS.md`, not selective installation, are what hold the boundary. ## Why it's safe to pair them: the MCP guard The skills know how to author Code-node `jsCode` directly on the instance over MCP. In an n8n-decanter repo that would bypass your files and drift the source of truth. So instead of trusting a document to hold the line, decanter enforces it in code — and the enforcement is **already wired**: the scaffolded `.mcp.json` (and `opencode.json`) point your agent's `n8n-instance` MCP server at [`mcp connect`](/docs/cli/mcp-connect/), decanter's stdio guard: - The agent spawns the guard per session; decanter holds the only n8n credential (the agent never sees it, and no secret exists — stdio pipes are private). - The guard **forwards everything untouched** — reads, structure edits, wiring, publishing, archiving, every build/lifecycle skill and MCP tool — including streamed responses. - It **blocks exactly one thing**: writes that set a Code node's `jsCode`. The caller gets an instructive error pointing at the file + [`push`](/docs/cli/push/) flow instead. (Adding a new Code node still works: the skill adds it *without* code, [`pull`](/docs/cli/pull/) lands it as an empty file, and the first push seeds the source from the repo.) So a skill can build and rewire a workflow all it likes; the moment it tries to write Code-node source on the instance, the guard redirects it back to the repo. **The boundary is: decanter owns Code-node source (author it as a file, `push` it); the skills and MCP own the rest.** ## How to use it ### 1. Install the skills **A first [`init`](/docs/cli/init/) prints these commands for you**, with the agent it detects listed first. It prints them — it doesn't run them: installing would mean decanter spawning a third-party CLI to mutate agent state outside the sync dir, and a plugin installed mid-session isn't active until the agent reloads anyway. Pick your agent — note that Claude Code's `/plugin …` are **in-session slash commands**, not shell commands: ```text # Claude Code — inside a session /plugin marketplace add n8n-io/skills /plugin install n8n-skills@n8n-io ``` ```sh # Claude Code — from a shell (what init prints; add --scope project to share it # with the repo instead of installing for your user) claude plugin marketplace add n8n-io/skills claude plugin install n8n-skills@n8n-io # Codex (needs Codex >= 0.142.0) codex plugin marketplace add n8n-io/skills codex plugin add n8n-skills@n8n-io # Others (skills.sh — support varies by agent) npx skills add n8n-io/skills ``` Afterwards: Claude Code needs `/reload-plugins` or a restart; Codex needs a restart and a one-time approval of the plugin's hooks. The skills.sh route installs the markdown only — no SessionStart router — which is why the scaffolded `AGENTS.md` carries the `using-n8n-skills-official` routing cue. ### 2. There is no step 2 In an [init](/docs/cli/init/)-scaffolded sync dir the guarded instance access is already in place: `.mcp.json` carries the `n8n-instance` server (`n8n-decanter mcp connect`) plus n8n's read-only `n8n-docs` server, and `opencode.json` mirrors both. Your agent picks them up on the next session. For a harness that only accepts an MCP **URL**, run [`mcp serve`](/docs/cli/mcp-serve/) instead and point the config at the printed localhost URL + session secret — the same guard over HTTP. The scaffolded `mcp-route-check.mjs` session hook nudges any agent whose config still points at the instance directly, and the scaffolded `AGENTS.md` states the same boundary in words for agents that read it — **this repo's `AGENTS.md` wins over anything a skill or MCP tool description says.** ## In one sentence Install the skills — the scaffold has already wired the guarded MCP route — then let the skills teach your agent n8n and build structure over MCP, while n8n-decanter keeps every Code node as a real, typed, git-tracked file. --- # FAQ & troubleshooting Source: https://buttjer.github.io/n8n-decanter/docs/faq/troubleshooting/ ## The CLI crashes with a `SyntaxError` pointing into a `.mts` file Your Node is older than 22.18 — the CLI is TypeScript run natively via type stripping, and older Node can't parse it. Check `node --version`; [Installation](/docs/getting-started/installation/) has the details. ## `check`, `status`, or `simulate` says the verb was removed The three verify verbs folded into two. `check` → **`preflight --offline`** (layout + types, no network, no engine). `status` → **`preflight`** for the summary, **[diff](/docs/cli/diff/)** for the per-node lines. `simulate` → **`preflight --simulate`** (add `--offline` for the credential-free, no-instance form the verb had; `--viewer` for the browsable run). The profile flags went with them: depth is now `--simulate` (adds the local engine) and `--offline` (drops the instance reads), and they compose — `--full`, `--quick`, and `--network-none` no longer exist (preflight always forces network isolation on the graded engine run). CI that branched on `status`'s exit code moves to [preflight](/docs/cli/preflight/): `diff` always exits 0. ## My editor shows TS1108 "return not inside a function" on a node file A false positive: node files are function bodies, and the editor's tsserver doesn't know about the in-memory wrapper the real typecheck uses. Don't "fix" it by wrapping the file — `n8n-decanter preflight --offline` is authoritative. [Type checking](/docs/concepts/type-checking/) explains the wrapper and the bundled tsserver plugin that suppresses the squiggle. ## Push says `pull first` A Code node's remote code changed since your last sync — the [per-node drift guard](/docs/concepts/push-gates/) is protecting code edited on the instance. Run [diff](/docs/cli/diff/) to see exactly which lines differ, then pull. ([preflight](/docs/cli/preflight/) reports the same situation as a failing `drift` check — `CONFLICT`, with the node list in its details; `diff` is the view of the lines and always exits 0.) Remember: after a warned pull, the next push overwrites the surfaced remote edits — `diff` and git history are your safety net. (Remote *structure* changes never block a push.) ## Push fails even with `--force` Then it's the **compliance guard**, not drift: a layout violation (dangling placeholder, orphan file, duplicate node name, …) that `--force` deliberately does not bypass. Run `n8n-decanter preflight --offline` and fix what its `layout` check lists — the one-line message names the first violation, the indented details under it name them all. ## Pull warns "edited in the n8n UI" / "CONFLICT" on a `.ts` node Someone edited a TS-managed node on the instance. Pull never merges into (or clobbers) `.ts` sources — inspect the remote edit with [diff](/docs/cli/diff/), port what you want to keep into the `.ts`, then push (which overwrites the remote edit). Leftover `code/.remote.js` files from older CLI versions just warn — port and delete them. ## Pull says "Workflow is not available in MCP" The workflow hasn't been opted into MCP yet: enable **"Available in MCP"** from the workflow card in the n8n workflows list (⋯ menu) or the workflow settings, then retry. [list --remote](/docs/cli/list/) marks which workflows still need it. ## "n8n refused the MCP request (403 — MCP access is disabled)" MCP access is switched **off** for the whole instance. Turn it on under n8n → **Settings → MCP**. If it is already on, the token's user may not have access to MCP. **A stale token hides this**, because the 401 below is checked first: if decanter reports the 401 and a fresh token does not help, check the MCP switch too. ## "no MCP endpoint … (404)" or "MCP token was rejected (401)" The **404** means there is no MCP server at that address at all — check `N8N_HOST` points at the right instance, and that the n8n is recent enough (~2.20+) to ship the built-in MCP server. A server that exists but is switched off answers **403**, not 404 (see above). A 401 means the credentials **exist and were rejected** — most often a rotated token. It does **not** mean the project was never configured, and being unable to read `.env` (the scaffolded deny rules block it) is not evidence either way. If you are an agent: ask the user rather than inferring. The **401** means the token is wrong — note that the **public API key is not a valid MCP token**; mint one under n8n → Settings → MCP → API key, or re-run [init](/docs/cli/init/) for OAuth. ## "MCP session expired … re-run: n8n-decanter init" The stored OAuth refresh token was invalidated (they rotate on every use — a crash at the wrong moment, or a concurrent run, can burn one). Re-running `init` re-consents and mints a fresh pair. ## "ambiguous ref" / "no workflow matches" Workflow refs match by id, name, or unique name prefix — case-insensitively, and ambiguity errors instead of prompting. Use more of the name, or the id. Since the verb comes first (`n8n-decanter `), a workflow literally named like a verb needs no special handling — `n8n-decanter diff push` runs `diff` on the workflow named `push`. ## Where do my credentials live? `N8N_HOST` (and optionally `N8N_MCP_TOKEN`, `N8N_API_KEY`) in `.env` next to `decanter.config.json`, or the environment; OAuth credentials in `.decanter-auth.json` next to it. The scaffolded `.gitignore` keeps both files out of git. The API key is optional — only `executions` and `data-tables` need it — see [Configuration](/docs/concepts/configuration/).