Releases: buttjer/n8n-decanter
Release list
v0.11.1
Added
N8N_DECANTER_AUTH=upstream— an auth mode for instances behind a proxy
that attaches the n8n credentials itself. In this mode decanter sends no
credential header at all: noAuthorizationon MCP, noX-N8N-API-KEYon
REST. That is the only thing that works — a proxy which adds its own key
appends it to whatever the client sent, and n8n answers401to the pair,
so the previous workaround (a placeholder value inN8N_API_KEY, to satisfy
the REST-verb guard) fails just like an empty one.N8N_API_KEYstops being
required, soexecutions,data-tablesandbackupwork with no key of your
own.mcp servedrops the agent's session secret instead of swapping it for a
credential, so that local secret is never forwarded upstream. Set it in.env
or with the newinit --auth upstream, which needs no token, no browser
and no TTY, and verifies both backends through the proxy. Existing credentials
are kept, ignored, and reported as unused. Any other value of the variable is
an error on every verb — a typo must not silently restore the header.
Fixed
- A REST 401 now says what is wrong. It used to fall through to a bare
401 Unauthorizedstatus line; it now namesN8N_API_KEYas invalid for the
host (or, in upstream mode, points at the proxy).
v0.11.0
Changed
-
Breaking: a compiled
.tsnode now identifies itself on line 1, not on
its last line. Open a bundled node in n8n and you used to see
var __n8n_node = {}followed by a few hundred lines of inlined helpers,
with the only trace of where it came from —// @ts-n8n sha256:…— at the
bottom, where nobody looks, naming n8n rather than the tool that put it
there. Push now writes one self-describing line where the editor opens:// n8n-decanter · workflows/orders/code/normalize-lines.ts · do not edit here · @ts-n8n sha256:39af5ea6… · v0.10.1 ca3c201 2026-08-20T09:14ZNothing you already pushed has to change. The old trailing form is read
forever and counts as fully in sync —push,diffandpreflightsay
nothing about it, and no write is queued just to relocate a marker. Nodes
adopt line 1 on their next real code push; there is no migration and no mass
re-push. The break runs the other way: an older n8n-decanter will not
recognise a node pushed by this version (its reader only looks at the last
line) and would pull such a node down as a plain.jsfile. Pre-1.0, that is
accepted rather than shimmed — upgrade the CLI everywhere that pulls the same
instance. The@ts-n8ntoken itself is unchanged, so onegrep -r @ts-n8n
still finds both forms. -
The layout guard now rejects a marker line in a node source file in either
position, and in.tsfiles too (it was trailing-only and.js-only).
That line is written bypush; it is never source. Surfaces as before via
preflight'slayoutcheck.
Fixed
-
An auth failure no longer sends you in a circle, and throttling is no
longer called an expiry. Every failed OAuth token refresh used to print
the same sentence —MCP session expired … re-run: n8n-decanter init— and
both halves of it could be wrong.initreuses.decanter-auth.json
whenever the host matches and never re-mints, so following the advice
re-probed with the same dead credentials and finished with "credentials
written anyway"; the only apparent way out was deleting a credential file
that was often perfectly fine. The three cases are now told apart:- the refresh token really is spent (
invalid_grant) — named as such,
and pointed atinit --reauth, a command that actually re-mints; - n8n is rate-limiting (429) — now retried with the same backoff the
MCP endpoint has always had (honouringRetry-After, five attempts), and
if it still fails it says the credentials are fine and never mentions
init. OAuth discovery got the same retry; both sat on barefetchcalls; - anything else — the reason is named without a diagnosis, and nothing
is suggested for discarding.
- the refresh token really is spent (
-
init's closing connection check acts on a spent token instead of
shrugging. It had the failure in hand and printed "credentials written
anyway". On a terminal it now offers to re-authorize on the spot; off one it
namesinit --reauth. Deliberately narrow — a network error, a 401, or a
403 "MCP access is disabled" still report as before, because none of them
says anything about your credentials. -
A successful first OAuth consent no longer warns "no MCP credentials
yet". The check looked for a pre-existing auth file, so a browser
authorization that had just succeeded still ended with advice to re-run
initwith a token. -
<verb> --helpprints help instead of running the verb.--helpwas
only recognised in argument slot 0, and every--flagis stripped before
dispatch, son8n-decanter init --helpwas indistinguishable from a bare
init— a request for help scaffolded a sync dir into whatever directory
it was asked from.--help(and-h) now wins from any position, before any
verb, any namespace and the picker. It prints that verb's own block rather
than the whole listing, with only the notes that apply to it;help <verb>is
the same question, and a barehelp/--helpstill prints everything.
Added
-
initrefuses to scaffold on top of a sync dir that already exists below
the target. Run from the root of a bigger repo whose sync dir lives in
n8n/,initused to drop a seconddecanter.config.json, the template,
workflows/,shared/,tsconfig.jsonand the agent configs into the root.
It now looks a few levels down first and stops, naming the sync dir it found
and the--dir=/N8N_DECANTER_DIRform that addresses it from where you are
— the same advice every read verb already gives. A terminal gets it as a
question (yscaffolds anyway); a piped or flag-driven run exits 1 having
written nothing, not even the target directory. Re-runninginitinside
an existing sync dir is unchanged. -
The provenance line carries the build stamp, not just the hash: the
node's source path relative to the sync dir, "do not edit here", the CLI
version, the git commit and the push time. The commit is HEAD at build time
and renders asca3c201+dirtywhen the sync dir has uncommitted changes —
which, withcommitOnPushon, is the normal case and reads correctly as
"built from working-tree state on top ofca3c201". Fields that cannot be
known (no git repo, unreadablepackage.json) are simply left out. None of
it is hashed, so a rename, a new commit or a CLI upgrade never makes a node
look changed — only the code below line 1 does. -
Credentials now resolve in a git worktree. Both
.envand
.decanter-auth.jsonare gitignored, so a fresh linked worktree had neither
and every credentialed verb died onN8N_HOST must be set— including the
mcp connectguard, which left agents in a worktree with non8n-instance
tools at all. A worktree without its own credentials now reads the main
checkout's copies (same path, resolved from git's own worktree pointer, no
gitsubprocess). A local file still wins, so a worktree deliberately
aimed at another instance keeps its own, and nothing else is redirected —
workflows/,.decanter.jsonanddecanter.config.jsonstay worktree-local.
For.decanter-auth.jsonthe shared file is the only correct shape: the
refresh token is single-use and rotates, so copying it into a worktree
(what Claude Code's.worktreeincludewould do) forks it into two token
chains and kills the loser. New Git
worktrees section covers the two
remaining worktree gaps decanter cannot fix — a missingnode_modulesand the
per-path MCP approval — and the troubleshooting entry for missing
n8n-instancetools gained the worktree case.
v0.10.2
Changed
-
"Missing
n8n-instancetools? Restart" is no longer the only answer we
give — in a nested sync dir it was a dead end. Agent wiring loads at
startup from the dir the agent was started in, so tools declared in
.mcp.jsoncan be absent for two reasons: the wiring is new (a restart fixes
it), or the wiring sits below the launch dir, where no restart will ever
load it. Every surface that taught only the first now teaches both, with the
discriminator you can apply yourself — is that.mcp.jsonbelow the
directory you started the agent in? — and the two working routes (start the
agent in the sync dir, or wire the root withN8N_DECANTER_DIRplus a
root-resolvable command): the scaffoldedAGENTS.md,init's own output
(which now prints the nested guidance instead of the restart line when it
scaffolds into a nested dir), Working with coding
agents, init and a new
troubleshooting entry. The CLI never needed
that wiring, sopull/push/preflightkeep working either way — the docs
now say so too. -
The oversized-scenario warning now names a remedy you can actually take.
Above 1 MBscenario createsaid "Trim it before that" — but there is no
trim flag, so the advice pointed nowhere. It now says what is true and what
to do: nothing is committed yet (scenario createnever commits — only the
nextpull/pushsweeps the folder into history), so you can still cut
items out ofdata.resultData.runDataby hand, or re-create the scenario
with--scaffoldinstead of--executionand author the pins yourself.
docs/cli/scenario.mdspells out both, including which parts of a capture
are unsafe to delete. -
The agent contract now says to orient before the first edit, not only
before the push.preflightwas always the read-only report of the
instance's side (drift,CONFLICT, a pendingparity), but every surface
framed it as the pre-push gate — so an agent edited first and learned about a
colleague's UI edit afterwards. The scaffoldedAGENTS.mdand both agent doc
pages now open the loop with it: on drift,pulland carry on; on a
CONFLICT, showdiffand ask before either side is overwritten. -
The scaffold stopped advertising a deny rule it no longer has. The
Claude Codesettings.jsondenies.decanter.json,.envand
push --force— but the scaffoldedCLAUDE.mdandopencode.jsonstill
claimed*.remote.jswas blocked too. Those conflict artifacts were removed
in the MCP pivot; both files now describe the policy that actually ships. -
Docs:
mcp connect/mcp serveno longer read as if the guard obtained
credentials itself. "decanter's own credentials", "the agent never holds an
n8n credential" and "no secret to manage" led readers (and agents) to believe
the guard handles the n8n login. It does not: it only reads whatinit
wrote to.env/.decanter-auth.json, and can at most refresh an OAuth
token. The mcp-connect / mcp-serve pages and the templateAGENTS.mdnow say
so outright — obtaining credentials is exclusivelyinit's job, and a
"no MCP credentials" answer means runinit, not retry. -
"decanter.config.json not found" now points at
init— with its flags.
The classic half-setup is a hand-written.env: an agent that cannot run the
browser OAuth flow asks its human to pasteN8N_MCP_TOKENinto a file and
stops there, leaving no config, template,.gitignoreor agent wiring behind.
The error now says the dir is not a sync dir yet, that.envalone is not
enough, and prints the prompt-free command that fixes it
(n8n-decanter init . --host <host-url> --token <mcp-token>). The docs
(init, configuration, troubleshooting, README) say the same thing: headless is
not a reason to skipinit— it takes the same token as a flag. -
init's restart reminder now covers everything it wires, not just
permission rules. MCP servers (.mcp.json/opencode.json— including the
guardedn8n-instanceserver), permission rules and hooks are all read at
agent startup, andinitnormally runs inside the session it configures.
The reminder now fires when any of those files is newly scaffolded and says
what it means: this session is still unconfigured, restart the agent (or
/reload). README, init and the agents docs say the same, and the scaffolded
AGENTS.mdtells the agent to ask for a restart when then8n-instance
tools are missing instead of connecting to the instance directly.
Fixed
- The scaffolded hooks now work when your sync dir is not where the agent was
started. All three found the sync dir by assuming it was the current
directory, which only holds for an agent launched inside it. With the sync dir
nested in a bigger repo — a layout the docs explicitly allow — the agent runs
at the repo root and every one of them misbehaved: the rename-reference guard
became a silent no-op, so$('Old Name')references left behind by a
renameNodewent unreported until a laterpushrefused them; the verify hook
spawned the CLI without a directory, so it blocked every node-file edit with
a "not a sync dir" error; and the MCP routing check scanned the wrong tree.
Each hook now locates the sync dir from its own installed path, so it behaves
the same wherever the agent starts. - The routing check no longer misses direct-route servers in your user
config. Its lookup for this project's entry in~/.claude.jsonmatched the
current directory, but that file is keyed by the repository root — so in any
sync dir inside a git repo the check silently found nothing. It now matches the
project entry for the sync dir or any parent of it. - The verify hook finds a locally installed CLI. It only ever looked for
n8n-decanteronPATH, so with a local (non-global) install it stayed quiet
and no verification ran at all. It now prefers the sync dir's
node_modules/.binand falls back toPATH. - "decanter.config.json not found" no longer sends you to
initwhen the sync
dir is simply somewhere else. Run from above a perfectly good sync dir —
what happens whenever an agent starts at the repo root — the error read as if
nothing had ever been set up, and advised scaffolding a second sync dir on top
of the working one. It now looks below the directory it searched from, names
the sync dir it finds there, and prints the--dir/N8N_DECANTER_DIRform
that reaches it. When there really is no sync dir, theinitadvice is
unchanged.
Added
-
The routing check also looks at parent directories, up to your repository
root. Agents merge.mcp.jsonfrom every directory above the one they start
in, so a server pointing straight at your n8n instance can sit in the repo
root's config and still route this session. The scan stops at the repository
boundary, so it never reaches into unrelated parent directories, and an
offender found above the sync dir is named by its relative path
(../.mcp.json) so you can tell which file it means. -
--dir <path>(orN8N_DECANTER_DIR) points any verb at a sync dir that is
not the current directory. The layout the docs allow but nothing supported:
the sync dir nested inside a bigger repo, with the agent started at the repo
root. The MCP entryinitwrites into the sync dir is invisible from up
there, and hoisting it to the repo root spawned the guard where no
decanter.config.jsoncould be found — so the guard did not work at all. The
search still only walks up; this says where it starts. In an agent's server
entry the environment variable is the form to reach for
("env": { "N8N_DECANTER_DIR": "flows" }), and relative values resolve
against the working directory, so a repo-relative one keeps working for
everyone who clones.initdoes not take--dir— it still takes the
directory to scaffold as an argument. -
initnow tells you how to wire an agent when your sync dir is nested in a
bigger project. Agents look for.mcp.json,opencode.jsonand
.claude/settings.jsonfrom the directory they were started in and never in
one below it, so everythinginitscaffolds is inert for an agent started at
the repo root above the sync dir. When init sees a project around it (a.gitor
package.jsonin a parent) it prints both shapes that work: starting the
agent inside the sync dir — recommended, nothing further to configure — or the
paste-ready MCP, opencode and hooks/permissions blocks for the project root,
with every path and glob already prefixed. That prefixing is the point: copied
up verbatim,Read(.env)/Edit(.env)guard the root's.envand quietly
stop protecting your credentials.initprints this; it never writes into a
parent directory. Only on the run that first scaffolds the agent files, and
never for a standalone sync dir.
v0.10.1
Added
-
The live mirror now tells the agent when it overwrote your work. It runs a
fullpullafter a structure edit, so it can replace an unpushed local code
edit with what is on the instance. It always warned about that — on stderr,
which is the one stream an MCP agent structurally cannot read, so the party
able to react never heard it. The warning now rides the result of the
agent's next tool call, naming the files and how to recover them from the
safety commit. Delivered once, never repeated.Only on
mcp connect(the transportinitscaffolds).mcp servepipes
upstream responses through untouched — including SSE — and buffering them to
inject an advisory line would break streaming for every response to deliver it
on some. On that transport the stderr warning stays the only signal. -
initnow says that its permission rules only bind the next session. It
writes.claude/settings.jsonwith the deny rules that keep an agent off
.decanter.json,.envandpush --force— but agents read permission config
at startup, not on change, andinitis normally run from inside the session
those rules are meant to constrain. They were silently inert until a restart,
and the docs mentioned a restart only for the skills plugin — so the rules that
actually gate the agent went unmentioned. Printed once, when the file is first
written; a re-init in a set-up directory stays quiet. -
node runfixtures can pin a node's other outputs, so$('Node').all(1)
finally answers. Give a node one items array per output and the branch
is readable offline:"nodes": { "Decide": [[{ "json": { "side": "true" } }], [{ "json": { "side": "false" } }]] }
all(1)/first(1)/last(1)and$items('Decide', 1)read output 1, and
an empty array is a real answer — that branch took no items.inputtakes
the same shape, indexed by the node's input (a Merge node's second input).
A plain items array still means a single output, so existing fixtures are
unchanged; asking for an output the fixture doesn't supply still refuses,
now saying how many it has. Until now every such call was refused outright,
because a fixture could only express one array per node. -
preflight --simulatenow replays a pinned node's other outputs too. The
stand-in decanter substitutes for a network node is a Code node, which has one
output — so an error output (or any second branch) captured in your execution
was replayed nowhere, and everything behind it sat with no input, emitted
nothing, and let the run pass. Each populated output now gets its own
stand-in, wired to the same input as the original and feeding exactly that
output's targets, so the branch really runs. Deliberately not wired to the
synthetic trigger: a stand-in fires only when the original would, so a replay
whose real nodes take a different path can't have the old branch's items
injected into it anyway. Thesimulatecheck names the splits in its details
(terminal and--json).teststill replaysmain[0]only — n8n's
pinDatais one flat items array per node, with no output dimension — and
scenario checknow spells out which of the two you are looking at. -
testnow reports what the run actually moved, not just that it
finished — a coverage line over the nodes that executed (enabled and
unpinned; a pinned node's items are the input you supplied):
coverage: 7/9 unpinned node(s) emitted items — 2 emitted none: Group products, Write rows. A node counts as emitting if it put an item on any
output. Some empty nodes are normal — a filter that dropped everything — so
the line warns and nothing more.But a run in which not one unpinned node emitted an item now fails
(exit 1), even with synthetic pins. n8n calls such a runsuccessand
it is: nothing errored. No data moved either, so nothing was demonstrated,
and reporting it as a pass was the check lying. The message names the usual
cause — a pin replays a node's first output only. -
scenario checkwarns about what the replay will throw away. Both replay
paths (test'spinData,preflight --simulate's stand-in node) read
main[0]only, while the validator happily accepts — and✓ valids — a
scenario carrying items on several outputs. The check now says so offline:
once for a node whose data populates more than one output, naming the indices
that get dropped, and once for a node source that reads a pinned node's
non-first output ($('Enrich').all(1),$items('Enrich', 1)) — the call that
returns nothing and leaves the node emitting nothing. Warnings only; the
scenario stays valid for the outputs that do replay.
Changed
- An explicit
.tsextension in a node file's import no longer fails the
typecheck. The scaffoldedtsconfig.jsonnow sets
allowImportingTsExtensions, soimport { total } from "../../../shared/money.ts"type-checks — until now it was rejected
(TS5097) even thoughpushbundles it without complaint, which made the
gate and the bundler disagree over a pure spelling choice. Extensionless
stays the recommended form (it survives a helper later becoming.js);
both spellings resolve everywhere. Existing sync dirs are offered the
updatedtsconfig.jsonon the nextinit— atsconfig.jsonyou edited
yourself is reported as drift and left alone, so add the option by hand
there. - Two of the four import rules for
.tsnodes now warn instead of
blocking a push: a relative import resolving outside the sync dir, and an
absolute-path import. Both only endanger the author's own portability — the
bundle still builds locally and fails loudly (Could not resolve) wherever
the target is genuinely absent — so blocking them was decanter making the
user's call. The advisory prints on every surface (preflight'slayout
details,push,node run) and exactly once per push;
preflight --fail-on=warnis the strict variant for CI. Node builtins
and npm packages not opted intobundleDependenciesstill block —
esbuild is silent about both, so without the block the failure would
surface at runtime on the n8n instance. - The scaffolded
mcp-route-check.mjssession hook now also inspects
user-level agent config for direct n8n MCP routes — Claude Code's
~/.claude.json(including its entry for the current project), Cursor's
~/.cursor/mcp.json, the VS Code user profile, and opencode's global
config. Previously it read only project files, so ann8nserver added
withclaude mcp add -s user(or any other user-scoped config) bypassed
the decanter guard without a word — exactly the "second door" the hook
exists to catch. Still a warning, never a gate. Re-runinitin an
existing sync dir (or re-copy the hook from the template) to pick it up. - The scaffolded
tsconfig.jsonnow covers the whole sync dir, not just
shared/andworkflows/— helper code may live in any folder inside the
sync dir (shared/is only the scaffolded default), so the typecheck and
the editor's tsserver now own every root without a config edit. Existing
sync dirs keep their scaffolded file; to match, widenincludeto
["n8n-globals.d.ts", "**/*.ts", "**/*.js"]and add
"**/backups/**", "**/executions/**", "decanter-ts-plugin", "dist"to
exclude. Two consequences worth knowing: a loose node-shaped scratch file
(top-levelreturnoutside any workflow'scode/) is now part of the
program and reports TS1108 — move it into a workflow or add its folder to
exclude; and wheninitscaffolds into an existing project that had
notsconfig.json, the new config sweeps that project's own.ts/.js
into the node-file typecheck — add your app dirs toexcludeif they
shouldn't gate pushes.
Fixed
diffandpreflightno longer report aCONFLICTfor a node with no
recorded sync hash. "Changed both locally and remotely" is measured against
the last-sync baseline in.decanter.json; with no baseline nothing is
known to have moved on the instance, andpushhas always treated that as
pushable. The two disagreed, so the report described a dead end the CLI did
not have — worse, its documented exit (push --force) is denied to agents by
the permission rulesinitscaffolds. Such a node now reads as
push pending, which is whatpushdoes with it.pullno longer warns
CONFLICTfor the same case on.tsnodes.- A
.js→.tsconversion no longer reads as data loss. Re-pointing a
node's//@file:placeholder is the sanctioned way to convert, andpush
andpullboth adopt it before doing anything — butdiffandpreflight
looked the file up in.decanter.jsonalone and announcedlocal file code/<node>.js missingfor the file you had just replaced. They now read the
placeholder too, so a converted node reportslocal changes in code/<node>.ts — push pending, identically for every converted node. preflightno longer prints✓ parity local code matches the draft
directly above✗ drift CONFLICT. Both checks read the same facts, so
paritymay claim a match only when every node is in sync; divergence that
driftowns is reported as aninfoline pointing at it.mcp connectsurvives an unreachable n8n instead of dying at the
handshake.initializewas forwarded like any other message, so a
connection failure answered the handshake with an error: the agent's MCP
client got noserverInfoand tore the session down before a single tool
call could report what was wrong. The guard now completes the handshake
itself when n8n does not answer, and the failure surfaces on the tool call
that needed the instance. Once n8n is reachable, the handshake is replayed
upstream so the session it uses is a real one. The startup line now reads
`gu...
v0.10.0
Added
-
scenario create <workflow> "<slug>" --extend— top an existing
scenario up with the pinnable nodes it is missing, keeping every value already
authored. Previouslyscenario createrefused an existing file outright, so a
scenariotestrejected could only be fixed by hand-editing raw JSON for nodes
the tool had never named. Also covers the ordinary case of a workflow that
gained a node after its scenario was written. -
scenario create --scaffoldnow works with no instance. The fill entries
were always built from your localworkflow.json; the instance only supplied
the per-node output JSON Schemas, which annotate the fill rather than
enable it. With noN8N_HOSTconfigured it now says the annotations are
missing and scaffolds anyway — each node lands as provenanceauthored
instead ofscaffolded, so the difference stays visible in the file. This is
what makespreflight --offline --simulatereachable on a plane: of the four
pin sources, only fetching a fresh capture actually needs n8n. The messages
that route you to a pin source now lead with the offline-viable ones, and
docs/cli/preflight.mdstates which of the four need the instance.
Changed
-
Breaking: a slug-less
scenario create --scaffoldnow writes
scenarios/scaffold.json, notscenarios/scenario.json. The old default collided with thescenario
verb, and the flag parser refuses to read a verb name as a flag value — so
preflight --simulate --scenario scenariofailed with--scenario needs a value, leaving the default file referenceable only as--scenario=scenario.
If you have a script that names the old default file, point it at the new one
(an explicit<slug>argument was, and stays, unaffected). -
scenario checknow reports thetestgate too, not just the
preflight --simulateone. The two demand different node sets on purpose —
--simulateasks only for nodes the capture reached,testasks for
every enabled non-pure node because it runs on the live instance with real
credentials. Reporting only the looser one meant a scenario could be green and
still be refused bytest.checknow says which gate you have passed, and
names--extendas the way to close the difference. The docs that asserted the
two rules were the same are corrected. -
scenario create --executionpins unreached nodes to an empty run instead of
asking you to invent output for them. A pinnable node the capture never
reached is written as[{"data":{"main":[[]]}}]— "this branch isn't
exercised" — and listed under_decanterScenario.notExercised. That makes a
capture-seeded scenario usable withteststraight away, keeps the node pinned
to zero items (so it can never touch the real world), and leaves the claim
visible for review: if a branch should have run, give it real data. -
The cold-start errors now name the non-interactive
init, and--mcp-token
is an accepted alias for--token. A fresh clone has no.env(it is
gitignored), soN8N_HOST must be set …is the first thing you — or a coding
agent — read. It said what was wrong and pointed only at a path that needs
someone at a prompt; a blind session diagnosed the problem in one command and
then had to hand the job back to a human. Both messages now spell out
n8n-decanter init . --host <host-url> --token <mcp-token>,init --hoston
its own names--tokenwhen it warns about missing credentials, and the token
flag accepts either spelling.
Fixed
-
The "install typescript" advice now pins
@^5. A bare
npm i -D typescriptinstalls 7.x, whose compiler is the native rewrite
and no longer exposes the programmatic API decanter's node-file typecheck
drives — so following the old advice replaced a skipped check with a broken
one. The skip message andpreflight's unlock now both say
npm i -D typescript@^5and name the reason.init's scaffold already pinned
^5; this only ever bit projectsinitdeliberately left alone. -
A 403 from the public API now names the scope you are missing. n8n answers
a valid-but-under-scopedN8N_API_KEYwith a bare 403 and says nothing about
which of eight scopes is absent. Every REST surface — executions, data tables,
backup — now gets a per-endpoint hint, including the trap that catches people
out:dataTable:readdoes not cover/columnsor/rows, which need
dataTableColumn:readanddataTableRow:read. The data-table hints also say
outright that decanter only ever reads them, so no write scope is needed. -
scenario createprints the file's size, and warns before it lands in git.
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. Nothing measured it, and
scenarios/is tracked — so the folder-wide auto-commit on the nextpullor
pushswept it into history unasked. The success line now carries the size,
and anything above 1 MB warns explicitly that it is about to be committed. -
Three scenario messages pointed somewhere the thing you needed was not.
A pre-rename scenario (_decanterMock) was told to look in
_decanterScenario.fill— a key not in the file; the message now names the key
it actually found. A replay gap derives its node list from the workflow
graph, so those nodes are by definition not infill, yet it said "see the
_decanterScenarioblock"; it now says they are not listed, and points at
--extend. And a node deliberately written as"Node": []was reported as
unfilled — it is now told apart from a node with no entry at all, with the
spelling for "emits nothing". -
The agent guard's 401 no longer reads as "this project was never set up".
It led with "runn8n-decanter init", and a blind field-test round watched an
agent conclude from it that there was no.envand no token at all — then send
its user through a pointlessinit. The.envexisted; the token had simply
been rotated. The guard now leads with the cause ("n8n rejected decanter's
existing MCP credentials … they are configured but no longer valid"), matching
what the CLI already said, and offersinitonly as the OAuth alternative. It
also maps 403 now, pointing at n8n → Settings → MCP. -
A missing
typescriptis reported as a skipped check, not a failed one.
preflight's node-file typecheck needstypescriptin your project. A
globally installed decanter ships none (it is a devDependency), andinit
leaves an existingpackage.jsonalone — so scaffolding into a project you
already had produced a module-resolution stack trace surfacing as a typecheck
failure, which reads like a type error in your own code. It is now an honest
skip, named in the coverage block with the one-command fix
(npm i -D typescript). -
The scaffolded
AGENTS.mdtells agents that.envis unreadable by policy
and that this is not evidence it is missing — the reasoning trap behind the
401 finding above. -
node runno longer answers a branch index with the wrong branch's data.
$('Node').all(1),$items('Node', 1)and$input.all(1)ask for a node's
second output — anIF's false branch, aSwitch's other case. A fixture
pins one items array per node, so there is no honest answer, but the
argument was accepted and ignored: you got output 0's items and the node
looked like it worked, graded against a shape it will never see live. Worse
than empty data, because nothing fails. These calls now refuse with a message
naming the call and the two ways forward (pin that branch as its own fixture
node, or run it for real withtest) — the same signpost pattern$varsand
$secretsalready use.n8n-globals.d.tsdeclared the parameter and
docs/cli/node-run.mdlisted the calls as fully covered, so both surfaces had
promised something the emulation never did. -
pullno longer destroys uncommitted local edits. It committed the folder
after overwriting it, so an uncommitted.jsedit was gone and had never
entered git — while the warning printed on that exact path told you to
"recover via git". Pull now takes a snapshot commit before it writes
anything (watchand the live mirror already did). If the snapshot cannot be
made — no git repo,commitOnPull: false, a git error — the pull still runs,
but the warning says the overwrite is not recoverable instead of promising
a safety net that isn't there. -
pull's clobber warning now fires for a node it has never synced. It was
gated on the node already having a sync baseline, which is backwards on the
read side: no baseline means the node isn't in.decanter.jsonyet, so the
local file is precisely the one with no protection. The loss path this opened
matches the scaffolded agent workflow exactly — an agent adds a Code node over
the guard (the guard blocksjsCode, so the remote body is empty), writes the
source intocode/<node>.js, and a background mirror pull lands before the
first push: fresh file replaced by the empty remote body, silently. -
The live mirror stops refreshing when its safety commit fails. It awaited
the commit and discarded the result, but that call returns a failure (it never
throws) for any git error — unset identity, a mid-merge tree,index.lock, a
rejecting hook. The documented "a dirty tree is safety-committed before the
pull" rail therefore degraded silently into an unrecoverable overwrite. It now
skips the refresh and says why, matchingwatch. -
Scenario gaps are now judged per branch, not per node. A branching node
(anIF, aSwitch) emits on one output per run, butpreflight --simulateandscenario create --executionread its first output for every
outgoing edge. Two consequences, b...
v0.9.0
Changed
-
The agent guard now refuses a
publish_workflowthat would take a broken
draft live.publishalready checked, but the raw MCP tool went straight
through the guard — so an agent could go live around the verb and ship exactly
the breakage the check exists to catch. Both transports (mcp connectand
mcp serve) run the same check on the same shared code.Fail-closed: if the check itself cannot run — n8n unreachable — the publish
is refused too, and the message says the check failed rather than claiming
the workflow is broken. A read that fails almost certainly means the publish
would have failed anyway, and "couldn't verify, so we shipped it" is not a gate. -
Dangling-reference checks now cover all four forms n8n rewrites on a rename
—$('X')(as before) plus$node["X"],$node.Xand$items('X').
Previously only the first was detected, so a rename could strand a$node[…]
call site that nothing reported:preflight,push,testandpublishall
passed it, and it failed at run time instead. The rule is n8n's own — its
rewriter handles exactly these four — so if n8n treats it as a reference, the
guard now does too.This can surface errors in workflows that passed before. A
$node["Old"]
reference to a node that no longer exists is a hard compliance error, which
--forcedoes not bypass. The message quotes the reference as written, so
it is clear which form triggered it. Computed references ($(someVar), a
template literal with${…}) are still left alone — a regex cannot resolve
them, and n8n has the same limit. -
Breaking:
n8n-decanter test <workflow>no longer executes. It used to
fall back to the newest capture underexecutions/and run the workflow for
real on your instance — a directory that is gitignored, so the same commit
behaved differently for different people, and a bare verb had real side
effects. Baretestis now a static tier: it reads the instance's draft,
reports dangling$('…')references, and runs nothing. Pass
--execution <id>or--scenario <slug>for the pinned run, which is
otherwise unchanged. There is no deprecation shim — atestthat still
executed sometimes would keep exactly the ambiguity this removes.The pinned run now also does the static check first, so a draft already known
to be broken is never fired at the instance. -
publishrefuses a draft carrying a dangling$('…')reference.
Previously nothing checked: the compliance guard runs onpush,preflight
andbackup— notpublish— so a task that only renamed nodes never hit a
gate and the break went live. The check reads the draft on the instance
(the readpublishalready makes), not your local folder:workflow.jsonis
a snapshot, so grading it would pass a broken workflow on a stale mirror and
block a legitimate publish from a fresh clone.
Added
-
initnow scaffolds a hook that catches stranded$('…')references right
after a rename. n8n'srenameNodeMCP op rewrites the node name and the
connections only, so the references it leaves behind used to surface at the
nextpush— arbitrarily far from the rename that caused them. On Claude Code
a PostToolUse hook onupdate_workflownow reports them immediately, split
into the two halves and in the order they must be repaired: other nodes'
expression parameters in n8n first, then the code files here, thenpush.It scans for the old name instead of running
preflight, deliberately: the
hook fires before the background snapshot refresh, and until that lands the
snapshot still carries the old name, so every reference still resolves and
preflightwould report clean. Silent when nothing references the renamed
node. The checklist in the scaffoldedAGENTS.mdremains the contract for
every agent — the hook is a reminder, not a replacement.
Fixed
-
Corrected the rename guidance: n8n's
renameNodeMCP op does NOT rewrite
$('…')references. The scaffolded agent guide (and the 0.6.0 release notes)
claimed n8n rewrites connections and$('…')references server-side on a
rename. Verified against real n8n 2.30.7 and 2.33.3: the MCP op rewrites the
node name and the connections only, then reports success with
validationWarnings: []— every$('Old Name')ref is left dangling, both in
Code-node source and in other nodes' expression parameters. The n8n editor
does rewrite them, but in the browser before it saves, so "server-side" was
wrong for that path too. No amount of pulling repairs this;pullfaithfully
mirrors what n8n stored.init'sAGENTS.md/CLAUDE.mdnow describe the real contract, including the
repair order that matters: fix other nodes' expression parameters over MCP
first, then local code, thenpush. The other order loses the code fix,
because a forwarded MCP write schedules a background snapshot refresh whose
pull overwrites unpushed.jsedits. -
A dangling-reference error now says which half it is and where to fix it.
The two compliance errors were near-identical and neither mentioned a rename,
which led toworkflow.jsonbeing hand-edited — turning the check green while
n8n stayed broken.
v0.8.0
Added
-
The interactive picker offers a
--forceretry when apushhits the drift
guard. Previously the error was printed and you were dropped back at the
menu, having to leave the picker and re-runpush --forceby hand — even
though the CLI had just told you--forcewould fix it. Now it asks:✗ remote code changed since last sync — pull first (or repeat with --force to overwrite the draft) retry with --force and overwrite the remote draft? [y/N]The default is No: a bare
Enter, or anything other thany/yes,
returns to the menu exactly as before. Answeringyre-runs the same menu row
(flags included) with--force, overwriting the n8n draft only — the
published version is untouched.It only appears for failures
--forcecan actually fix. A layout
compliance error never prompts, because forcing does not bypass it. And this
is the interactive picker session only: piped and non-interactive runs never
prompt — they print the--forcehint and exit non-zero, unchanged. -
diff— the new verb for "show me the actual changed lines". It is the
promoted half ofstatus --diff: per-node unified line diffs of your local
code against the n8n draft,.tscompiled first (bundlingshared/*, so a
helper edit shows every importing node). Nodes that are in sync are omitted
entirely, and a clean tree says so in one line. Multi-ref likepull/push;
no workflow on a terminal opens the picker.It always exits 0.
diffis an inspection view, likegit diff— the
gate ispreflight. See the migration note under Removed. -
preflight --viewer(with--simulate): leaves a browsable throwaway
n8n running so you can open the replayed run in the UI — the interactive half
of the oldsimulateverb, now explicit instead of implied by a TTY. It does
not relax preflight's safety contract: the graded run stays headless with
--network-none, and the viewer is a second, separate container. A workflow
with a multi-batch loop reportssimulateas skipped under--viewer
("a preview, not a pass/fail check"), never as a pass. -
preflight --no-typecheckskips thetypescheck — the escape hatch the
retiredcheckverb had. -
The agent guard now logs a startup line and an audit trail (
mcp connect
andmcp servealike, on stderr):guard: connected to <host> — forwarding all n8n MCP tools, blocking jsCode writes in update_workflow guard: forwarded search_workflowsPreviously the guard spoke only when it blocked something, so an empty log
meant either "ran, blocked nothing" or "never started" — indistinguishable,
and opposite in meaning. The startup line settles that; the per-call lines
make the guard the one place that can answer what did an agent actually do
to my n8n instance?, since every MCP call passes through it.Tool names only — arguments are never logged, so the log stays safe to
attach to a bug report. -
Every
preflightfinding can now carrydetails[]— the full list behind
the one-line message: every layout violation, everytscerror, the
drifted node names, the viewer URL. Printed indented under the check line,
and present in--json. This is how the informationcheckprinted in full
survives its removal; without it, foldingcheckintopreflightwould have
truncated a 12-violation layout failure to its first line.
Changed
-
The picker lists pulled workflows newest-synced first, instead of the
folder's alphabetical order — the workflow you last pulled or pushed is under
the cursor when the picker opens. Unpulled remote rows keep their place after
the local ones. The signal is each workflow folder's sync timestamp, so it is
local activity, not committed history: right after a freshgit clone
everything looks equally recent and the list falls back to alphabetical until
your first pull or push. Scriptedlistoutput is deliberately unchanged. -
The CLI banner's
n8nwordmark now uses the brand orange, matching the
website (#E18528, derived from the site's accent color) rather than ANSI
red. It degrades gracefully — a 256-color terminal gets the nearest orange, a
16-color one keeps the old red — and piped output andNO_COLORstay plain,
exactly as before. -
Breaking:
preflight's profiles are replaced by two orthogonal flags.
--fulland theProfilemodel are gone. Depth is now--simulate
(additive — appends the local-engine run of your code) and--offline
(subtractive — drops the instance-reads tier), and they compose:preflight static + instance reads (the default gate) preflight --simulate + a local-engine run of your code preflight --offline static only — no instance contact preflight --offline --simulate static + local engine, no instanceMigration:
--full→--simulate. And read the next entry carefully —
--offlinestill exists but means something narrower. -
Breaking:
preflight --offlineno longer runs the local-engine replay.
It used to mean "static + engine, no instance"; it now means "static only".
The flag name is unchanged, so nothing will error — an air-gapped CI job
onpreflight --offlinesimply stops running the engine and quietly loses
that coverage. Migration:preflight --offline --simulateis the old
--offline. (This narrowing is also what makes--offlinefast enough for
the per-edit hook: it now spawns no Docker container.) -
Breaking:
preflight --jsonreplacesprofilewithflags. Where the
report carried"profile": "default" | "full" | "offline"it now carries
"flags": {"simulate": false, "offline": false}. Agents key on this. Every
other field is unchanged, and each entry inchecks[]gains an optional
details: string[]. -
preflightwith no workflow and an empty"workflows"config now checks
every pulled workflow instead of erroring with "no workflow ids" — the
behaviour thecheckverb had, kept now thatpreflightabsorbs it. -
A workflow folder with an unreadable
.decanter.jsonno longer fails your
gate.checkscanned folders, so a corrupt state file anywhere under
workflows/was a hard error for the whole run.preflightgrades resolved
workflows, and a folder whose state won't parse can't resolve to one — so
it is named in a warning (corrupt .decanter.json (…) — skipping this folder) and skipped, while every healthy workflow is still graded. The fact
is still reported; it just no longer blocks work on unrelated workflows. -
preflight --simulateaccepts multiple workflows. The oldsimulate
verb took exactly one; preflight loops, so a multi-ref run spins one engine
container per workflow, serially. -
The scaffolded template now runs
preflight --offlinewhere it ran
check— the PostToolUse verify hook, bothpackage.jsonscripts, and the
agent-facing prose inAGENTS.md/CLAUDE.md. Existing sync dirs keep
their files: re-runn8n-decanter initto be offered the refresh, and note
that init leaves locally-modified files alone, so a hand-edited hook or
package.jsonstill invokes a removed verb until you update it yourself. -
The scaffolded agent permission allowlist swaps its
check/status/
simulaterules for adiffpair (both the bare andnpxshapes);
preflight --simulateis already covered by the existingpreflight:*rule. -
The interactive picker's action menu is now
preflight,preflight --simulate,diff,pull,push,watch,executions— a menu row may
carry flags, which is how the browsable local-engine run survives the fold. -
The scaffolded agent contract now treats
pushas part of finishing the
work, and reserves "ask the user first" forpublish. A push lands on the
workflow's draft and never changes what is running; onlypublish/
push --publish/unpublishdo. The old rule gated both behind "only when
the user asks", so an agent handed "build me an hourly job that tags orders"
would build the structure in n8n, write and verify all the Code, and then
stop — leaving every Code node empty on the instance and the real code in
the repo, reporting "ready to push". Correct by the old rule, and not what
anybody asked for. Agents are now told to push once offline checks pass, to
say what landed, and to still ask first when the workflow is published/active
or a teammate is editing it. Going live remains a deliberate, user-requested
step. Affectstemplate/AGENTS.md.example(copied into new sync dirs by
init) and the/docs/agentssurfaces. (Surfaced by the Plan 35 blind field
test, where the "failing" agent was following the old contract exactly.) -
The scaffolded agent contract now follows the
preflight → push → test → publishflow.preflightis local-only (it no longer runs on the instance),
sotest— which runs the workflow's draft — is only meaningful after a
push. The old contract framedtestas a pre-push runtime check and ended its
loop atpreflight → push; both are now reconciled to the new order. Affects
template/AGENTS.md.exampleand the/docs/agentssurfaces. (Same surface
the Plan 60 verb reorder changed — kept in lockstep so the blind field test
grades agents against a contract that matches the tool.) -
Breaking:
preflightno longer runs the instance-sideteststage. It
rantest_workflowagainst n8n's draft, while every other stage graded
your local files — so whenever a push was pending, one score described
two different versions of the workflow, flagged only by a-10parity warn.
A report could read caution, 90/100 while its runtime evidence was about
code you weren't shipping.preflightnow grades ...
v0.7.0
Added
-
checknow warns when local work has not been registered with n8n — a
node whose//@file:placeholder has moved off what.decanter.jsonrecords
(the shape of a.js→.tsconversion), or whose recorded file is gone from
disk. It stays a warning, not an error:pushreconciles the file map, and
the compliance guard runs before that reconcile, so failing here would refuse
the one command that fixes it. -
n8n-decanter --versionprints the installed version (-vtoo), the way
every CLI is expected to. It answers before any config load or verb dispatch,
so it works from anywhere — including outside a sync dir. Passed alongside a
verb it is a hard error naming the flag you meant, so a stray--version
can't quietly swallow a command. -
A first
initpoints at n8n's official skills pack. Setup now closes by
naming n8n-io/skills — the knowledge layer
that makes agentic workflow building work — and printing the install commands
for Claude Code, Codex, and skills.sh, with the agent it detects
from your environment listed first and the activation step each one still
needs. It prints; it does not install: that would mean 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. Said once per sync dir (no
re-init repeats it), on every path including piped and--host-driven runs,
and it consumes no input — no existing script's stdin changes. (Plan 55.) -
initcan run non-interactively via--host/--token/--api-key.
Passing any of them drives setup purely from the flags plus any existing
.envand issues no prompt — so a script or coding agent can bootstrap a
sync dir without the interactive stdin dance (the field-test agents needed
20+ tries to drive the old prompt path).--hostis required in this mode
(a scheme-less local host is normalized tohttp://, like a typed one) and
wins over an existing.envvalue;--tokensetsN8N_MCP_TOKEN(headless
OAuth is still terminal-only);--api-keysets the optionalN8N_API_KEY.
The flag-less path (interactive, or piped answers) is unchanged. (Plan 35
field-test finding.) -
node runnow emulates$jmespath. A Code node that calls
$jmespath(data, expr)(or the$jmesPathalias) runs offline, matching
n8n's result (backed byjmespath@0.16.0, the version n8n pins). It also
fills in$items()/$node(views over the fixture'snodes),$vars/
$secrets(new fixture fields), and$nodeId/$nodeVersion/$webhookId. -
node runfixtures gainedvarsandsecretsto pin the instance-scoped
$vars/$secretswhen a node reads them.
Changed
-
Breaking: the scaffolded Claude Code settings moved from
.claude/settings.local.jsonto.claude/settings.json, andinitnow
migrates existing sync dirs. The file holds project policy — decanter's verb
permissions plus theverify.mjsandmcp-route-check.mjshooks — with
nothing machine-specific in it; it was already being committed and tracked in
the shared.decanter-template.json, solocalwas the wrong scope, and it
squatted the one file Claude Code reserves for your own overrides. The
local slot is now yours: permission lists merge across the two files and a
denybeats anallow, so a local file can add to the policy but cannot
unblock what the project denies. On re-init, an untouched copy is moved for
you; a copy you edited is left exactly where it is and the new file is not
written (both would register their hooks) —initsays what to move, and
--forceresolves it by removing the old file. Asettings.local.jsonthat
initnever wrote is never touched. (Plan 56.) -
Breaking:
backup restoretakes the backup as an argument, not a flag —
backup restore <workflow> [<backup>].--version <id>and--at <ts>
are gone. The argument is a backup ref resolved by shape, exactly like a
<workflow>ref: paste a timestamp (or a prefix — a bare date is enough) or a
versionId(short or full), whichever column ofbackup listyou have to
hand.backup restore order-sync 2026-07-24andbackup restore order-sync a1b2c3d4both just work; a ref that matches nothing is an error, never a
silent fall back to the latest. The retired flags fail loudly with the
replacement. This also un-squats--version, which no CLI can spend on a
verb-scoped meaning (see Added). -
check's success line now states its scope —
OK (local layout — status compares with n8n)instead of a bareOK.check
is offline by definition, so green means "well-formed", never "live in n8n".
The agent guide gained the matching rule, and the.js→.tsrecipe now ends
atpushrather thancheck— it previously told you to stop one step short
of the conversion actually reaching the instance. (Surfaced by the Plan 35
blind field test: three separate sessions authored code, read a greencheck
as "done", and never pushed.) -
scenario create/scenario checkwith no workflow now open the picker on
a terminal, like every other ref-taking verb (pull,push,backup …, …).
They previously hard-errored with a usage line even on a TTY, which made them
the odd ones out. Piped / non-TTY runs are unchanged — still the usage error —
so scripts and agent harnesses never block on a prompt. (Surfaced by the Plan
35 blind field test, where an agent tripped the inconsistency twice.) -
node runsignposts instead of crashing on instance-scoped globals. A
global whose value lives on the running instance ($vars/$secretswhen
unpinned,$evaluateExpression) now throws a friendly message that names the
global and points totest(or the fixture field) — never a bare
ReferenceError.docs/cli/node-run.mddocuments the covered / partial /
unsupported boundary.
Fixed
-
The documented Claude Code skills-install commands are no longer
copy-paste-broken./plugin marketplace add//plugin installare
in-session slash commands, but the docs and the scaffoldedAGENTS.md
presented them as shell commands. Both now show the in-session form and the
real shell equivalents (claude plugin marketplace add …/
claude plugin install …) separately, plus the post-install activation step
each agent needs. -
A
.js→.tsconversion is no longer reverted by a pull that fires before
the first TS push. Re-pointing a node's//@file:placeholder to a.ts
file and swapping the source is the sanctioned way to convert a node, but a
pulllanding in the window before the first TSpush— notably the
on-by-default live-mirror background refresh after a structure edit — rewrote
the placeholder back to.jsand left.decanter.jsonpointing at the
deleted.jsfile, so the next push failed withreferenced node file missing. Pull now honors the re-pointed placeholder exactly as push does
(they share one reconcile step). (Plan 35 field-test finding.) -
initno longer breaks localhttpinstances. A scheme-less host typed
at then8n host:prompt now defaults tohttp://for local addresses
(localhost, loopback, private LAN ranges,*.local) andhttps://
otherwise. Previously every scheme-less host gothttps://, so a local n8n
(plain http) was written to.envas a TLS URL and every sync/guard call
failed withfetch failed. A scheme you type is still kept as-is. -
n8n-globals.d.tsno longer over-declares$if/$min/$max. Those are
n8n expression-language helpers ({{ }}only), not Code-node globals — they
throw in a real Code node too — so declaring them wrongly type-checked broken
code. The declared surface now matches what a Code node actually sees, and is
single-sourced (init copies the one root file — no duplicate template copy). -
The scaffolded agent permission allowlist (
.claude/settings.json— see the
move under Changed) now
pre-approves the read-onlypreflightgate, so an agent following the
template's recommendededit → check → preflight → pushloop no longer stalls
on a permission prompt at the gate itself. Also dropped the obsolete
*.remote.jsdeny rule — those artifacts were removed in the Plan 32 MCP pivot.
v0.6.0
Added
backup— git-native, redeployable disaster recovery.n8n-decanter backup create <workflow>captures the workflow's full REST export into a
committed, versionedworkflows/<slug>/backups/<timestamp>.<versionId>.json
store — the fidelity MCP can't give (credential refs +descriptionkept;
pinData/staticDatastripped; each Code node'sjsCodestays a//@file:
placeholder, so no code is duplicated). It dedupes on an unchanged
versionIdand rolling-prunes the working set tobackupLimit(config,
default 20;0keeps all).backup restore <workflow> [--version <id> | --at <ts>]re-inlines the Code fromcode/and REST-POSTs a new,
unpublished workflow with node ids preserved — a real second version
history that survives the instance being lost; it prints credential-rebind
hints + the editor URL (publish is your next step).backup list <workflow>
shows the retained set. REST-only: needsN8N_API_KEY. The backup file is
not auto-committed (it carries credential refs and any embedded
secrets) — review it, thengit adddeliberately.- Live
workflow.jsonmirror — the review snapshot refreshes itself after
an agent restructures a workflow through the guard. When a structure edit
is forwarded throughmcp connect/mcp serve(a non-blocked
update_workflow), decanter now schedules a debounced backgroundpullof
that workflow, so the read-onlyworkflow.json(+ code files + state) stays
fresh with no manualpull. On by default; set"liveMirror": falsein
decanter.config.jsonto disable (CI / deterministic setups). It is
fire-and-forget (never blocks the agent's next tool call), git-gated
(safety-commits before pulling; skips with no git), and tracked-only. This
changesmcp connect/servedefault behavior (additive and disable-able —
not breaking). preflight— the whole verification ladder as one scored, read-only
gate.n8n-decanter preflight [workflow…]runs every safe check there
is — local static (layout,types) → instance read-only (connect,
access,parity,drift,snapshot,lifecycle,history,
capture) → pinned draft runs (test,simulate) — ordered fast→slow,
streaming each result, and condenses them into a score (0–100) and a
verdict (ready/caution/not ready, exit 0/1) with per-check
remediation. Profiles are explicit and deterministic:--quick(static +
sync), default (+test),--full(+simulate),--offline(static +
simulate, no instance). It brings executions into the gate — auto-
fetching the newest capture whenN8N_API_KEYis set (--no-fetchopts
out) and reading production run health (history, via MCP
search_executionsor the REST fallback). Coverage is first-class: every
skip names its unlock, and--require=<ids>turns a skipped check into a
hard fail;--fail-on=warnpromotes a caution to exit 1;--fail-fast
stops at the first failure.--jsonemits the full report (stable check
ids + remediation strings — the agent contract).preflightnever
mutates in any profile: no push, publish, restore, or draft write —
testruns in a never-mutate mode andsimulateheadless with
--network-noneforced on. The single gate to run beforepush/publish.test— instance-side pinned test runs (the recommended runtime
check).n8n-decanter test <workflow>runs the workflow on your
instance via MCPtest_workflow: the trigger and network/credentialed
nodes are pinned from a capture (--execution, default newest) or a
committed scenario (--scenario), logic nodes execute for real on the
instance-exact engine, and each node's output is diffed against the
capture (exit 1 on divergence;--triggerpicks the start node,
--jsonemits the report). The run targets the draft — the live
version is never affected. On a terminal, when local code differs from
the draft,testoffers to push it first (drift-guarded, draft-only)
and afterwards to keep or restore the pre-test draft (n8n version
history when available, byte-exact write-back below n8n 2.29);
non-interactive runs never mutate and say when they tested the draft
instead of local code.simulatestays the offline sibling —
pre-push/CI/isolation/version-rehearsal — and its docs now recommend
testfirst.mcp connect— the stdio MCP guard, auto-wired byinit. The default
way a coding agent reaches your instance's MCP server: the scaffolded
.mcp.json(andopencode.json) carry a static, secret-free
n8n-instanceentry ({"command":"n8n-decanter","args":["mcp","connect"]}),
so guarded instance access exists the momentinitruns — nothing to
start, no secret to manage (stdio pipes are private). Decanter holds the
credentials; the same guard rule asmcp serveapplies (see below).
Structure and lifecycle acts — creating/renaming/archiving workflows,
adding/renaming/wiring nodes — pass through; Code-node (jsCode) writes
are blocked toward the file +pushflow. Fail-closed on unparseable
input; an unreachable instance answers the agent with a JSON-RPC error
naming the host; logs go to stderr (stdout is protocol-only).mcp serve— the same guard as a localhost HTTP proxy, for agents
configured by URL: decanter holds the credentials (the
agent gets a per-session secret instead), every read and structure
operation forwards untouched (SSE included), and exactly one thing is
blocked —update_workflowcalls that write Code-node source, via either
ajsCodekey or asetNodeParameterop whose path targetsjsCode,
which get an instructive "edit the file + push" tool error. Fail-closed on unparseable
bodies, 127.0.0.1-only, body-size cap; the running endpoint + secret land
in a gitignored.decanter-proxy.json. The template gains a
mcp-route-check.mjssession hook that nudges agents whose MCP config
still points at the instance directly, and the sync-dirAGENTS.md
contract is now guard-first.
Removed
- Breaking: the structure/lifecycle verbs are gone —
rename,create,
node create(and its--tsflag), andnode rename. Those acts go
through n8n itself: the n8n editor, or n8n's MCP tools reached through
the newmcp connect/mcp serveguard (which is exactly what the
official n8n skills drive). Decanter's job is the reconcile: the next
pullre-caches a renamed workflow's name (folder stays put), renames a
renamed node's local file, and lands a new Code node as a source file. A
Code node added over MCP carries nojsCode(the guard blocks code in
addNode) — it now lands as an empty file whose firstpushseeds
the source, completing the guarded authoring loop. Two behaviors did not
survive the removal:$('…')refs inside local.tssources are no
longer rewritten on a node rename (n8n never sees.ts— update them by
hand after the pull), and validate-before-create is now the calling
agent's discipline (validate_workflowfirst, as the n8n skills teach). - Breaking: the
deleteverb is gone. Decanter no longer offers a hard
delete; retiring a workflow is an n8n act (archive it over MCP or in the
UI — reversible there, which is also where permanent deletion lives). - Breaking: the
duplicateverb is gone. MCP has no lossless full-JSON
create, so a faithful clone required the public API — rather than keep the
API dependency or ship a lossy SDK-code re-expression, the verb was
dropped. Duplicate workflows from the n8n UI andpullthe copy. - Breaking:
watch's browser-reload proxy is gone —browserReloadand
proxyPortconfig keys are no longer honored (silently ignored, not an
error). n8n 2.x reflects an MCP draft edit in the open editor natively
(soft canvas re-render, skipped — with a warning — while the tab has
unsaved edits), making decanter's injected<script>-reload proxy
redundant and, on that exact dirty-tab path, worse than doing nothing (a
hard reload would have clobbered the unsaved edits).watchnow just
prints the editor deep link with a note to keep the tab open; it updates
live on every push. - Breaking:
simulate --pinand per-nodefixtures/are gone — folded into
scenario. The per-nodeworkflows/<folder>/fixtures/<node>.json
mechanism and its precedence over captures are removed outright; a scenario
is now the only committed pin artifact and is always self-contained (no
fixture-over-capture layering to reason about).--pin's job — "make a
clean capture reproducible" — is nowscenario create --execution <id>. A
leftoverfixtures/dir is a hard error fromsimulate/checknaming
the replacement; there is no silent read-path or auto-migration for it
(unlike a leftovermocks/dir, which auto-migrates toscenarios/on
first touch — see thescenarionamespace under Added).
Fixed
- Verb-first error hints. Several CLI error/guidance messages suggested
verb-last commands (n8n-decanter <ref> simulate …,
n8n-decanter <ref> executions,n8n-decanter <ref> scenario …) that the
verb-first grammar rejects when copy-pasted; every one now prints the
verb-first form (n8n-decanter simulate <workflow> …,
n8n-decanter executions <workflow>,n8n-decanter scenario … <workflow>). - Refresh-token race (OAuth): two concurrent MCP calls — or
watchplus
a manualpushsharing.decanter-auth.json— could both redeem the
single-use refresh token, killing the session for the loser ("re-run
init"). Concurrent calls now share one redemption, a lost cross-process
race recovers by re-reading the winner's rotated auth file, and auth-file
writes are atomic. - MCP client hardening: a transient handshake failure no longer poisons
every later call in the same run; a 200-with-HTML answer (captive
port...
v0.5.0
[0.5.0] - 2026-07-21
Changed
- Breaking: verb-first grammar. The verb now comes first —
n8n-decanter <verb> [workflow…]. Verb-last (n8n-decanter wf123 push) is no
longer accepted and errors with unknown verb. Because everything after the
verb is an argument, a workflow named like a verb needs no special handling:
n8n-decanter status pushrunsstatuson the workflow namedpush. Flags
may still appear in any position. - Breaking: node operations moved under a
nodenamespace.add→
node create <workflow> "<Node name>", the two-name node rename →
node rename <workflow> "<old node>" "<new node>", andrun <node-file>→
node run <node-file>. - New workflow folders are kebab-case (
Order Sync→workflows/order-sync/)
instead of keeping spaces and capitals. Existing folders are left untouched
and still resolve as refs — no migration, no churn. - A workflow folder no longer follows a remote rename. The folder is a stable
local slug; the always-current display name lives in.decanter.json(see
Added). Renaming a workflow (locally or on the server) never moves your folder.
Added
data-tablesverb — a read-only fetch of n8n data-table schemas and
rows (the built-in project-scoped tables, n8n ≥ 2.x) into a top-level,
gitignoreddata-tables/<table>/{meta,columns,rows}.jsondir, for developing
and debugging against real table contents offline.--filter '<json>',
--search, and--sortpull only a slice of a large table server-side (the
applied filter is recorded in each table'smeta.json);--limit/--all
control page size and exhaustion. It never writes a data table.
data-tables cleanremoves the dir (offline). Gated by the newdataTables
config key (defaulttrue); when off, the fetch refuses and the recommended
key needn't carry the data-table read scopes (dataTable:list,
dataTable:read,dataTableColumn:read,dataTableRow:read)..decanter.jsonnow caches the workflow's displayname(refreshed on
every pull), so the picker,list, and ref-resolution show the real name even
though the folder is a kebab slug — and keep working ifworkflow.jsonis
missing or corrupt.list --jsonemits[{ name, id, dir }]for tooling (remote-only
workflows under--remotehavedir: null).- No-ref → picker. A ref-taking verb given no workflow, on a terminal, opens
the interactive picker to choose one and runs the verb on it. Piped/non-TTY
runs keep the config-default / error behavior, so scripts and CI never block. simulatenow replays single-iteration loops. A workflow whose only
repeated node is asplitInBatches("Loop Over Items") driver that ran a
single batch — it runs twice (one batch pass + the final "done" pass) while
every other node ran once — no longer hard-errors. The loop driver executes
for real to reproduce the loop, and each node's one captured run pins exactly.
Multi-batch loops (any node ran more than once) stay out of scope, since
first-run-only pinning can't feed later iterations.
Removed
- Breaking:
rename --workflowflag. Workflow rename is now the single
top-level formrename <workflow> "<new name>"; node rename lives under
node rename.