Skip to content

Releases: buttjer/n8n-decanter

v0.11.1

Choose a tag to compare

@buttjer buttjer released this 10 Sep 13:14
c2314e7

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: no Authorization on MCP, no X-N8N-API-KEY on
    REST. That is the only thing that works — a proxy which adds its own key
    appends it to whatever the client sent, and n8n answers 401 to the pair,
    so the previous workaround (a placeholder value in N8N_API_KEY, to satisfy
    the REST-verb guard) fails just like an empty one. N8N_API_KEY stops being
    required, so executions, data-tables and backup work with no key of your
    own. mcp serve drops 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 new init --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 Unauthorized status line; it now names N8N_API_KEY as invalid for the
    host (or, in upstream mode, points at the proxy).

v0.11.0

Choose a tag to compare

@buttjer buttjer released this 03 Sep 10:31
76ae1de

Changed

  • Breaking: a compiled .ts node 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:14Z

    Nothing you already pushed has to change. The old trailing form is read
    forever and counts as fully in sync — push, diff and preflight say
    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 .js file. Pre-1.0, that is
    accepted rather than shimmed — upgrade the CLI everywhere that pulls the same
    instance. The @ts-n8n token itself is unchanged, so one grep -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 .ts files too
    (it was trailing-only and .js-only).
    That line is written by push; it is never source. Surfaces as before via
    preflight's layout check.

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. init reuses .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 at init --reauth, a command that actually re-mints;
    • n8n is rate-limiting (429) — now retried with the same backoff the
      MCP endpoint has always had (honouring Retry-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 bare fetch calls;
    • anything else — the reason is named without a diagnosis, and nothing
      is suggested for discarding.
  • 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
    names init --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
    init with a token.

  • <verb> --help prints help instead of running the verb. --help was
    only recognised in argument slot 0, and every --flag is stripped before
    dispatch, so n8n-decanter init --help was 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 bare help/--help still prints everything.

Added

  • init refuses 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/, init used to drop a second decanter.config.json, the template,
    workflows/, shared/, tsconfig.json and 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_DIR form that addresses it from where you are
    — the same advice every read verb already gives. A terminal gets it as a
    question (y scaffolds anyway); a piped or flag-driven run exits 1 having
    written nothing, not even the target directory. Re-running init inside
    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 as ca3c201+dirty when the sync dir has uncommitted changes —
    which, with commitOnPush on, is the normal case and reads correctly as
    "built from working-tree state on top of ca3c201". Fields that cannot be
    known (no git repo, unreadable package.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 .env and
    .decanter-auth.json are gitignored, so a fresh linked worktree had neither
    and every credentialed verb died on N8N_HOST must be set — including the
    mcp connect guard, which left agents in a worktree with no n8n-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
    git subprocess). A local file still wins, so a worktree deliberately
    aimed at another instance keeps its own, and nothing else is redirected —
    workflows/, .decanter.json and decanter.config.json stay worktree-local.
    For .decanter-auth.json the 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 .worktreeinclude would 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 missing node_modules and the
    per-path MCP approval — and the troubleshooting entry for missing
    n8n-instance tools gained the worktree case.

v0.10.2

Choose a tag to compare

@buttjer buttjer released this 19 Aug 04:47
c4e6ec6

Changed

  • "Missing n8n-instance tools? 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.json can 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.json below the
    directory you started the agent in?
    — and the two working routes (start the
    agent in the sync dir, or wire the root with N8N_DECANTER_DIR plus a
    root-resolvable command): the scaffolded AGENTS.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, so pull/push/preflight keep working either way — the docs
    now say so too.

  • The oversized-scenario warning now names a remedy you can actually take.
    Above 1 MB scenario create said "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 create never commits — only the
    next pull/push sweeps the folder into history), so you can still cut
    items out of data.resultData.runData by hand, or re-create the scenario
    with --scaffold instead of --execution and author the pins yourself.
    docs/cli/scenario.md spells 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.
    preflight was always the read-only report of the
    instance's side (drift, CONFLICT, a pending parity), 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 scaffolded AGENTS.md and both agent doc
    pages now open the loop with it: on drift, pull and carry on; on a
    CONFLICT, show diff and ask before either side is overwritten.

  • The scaffold stopped advertising a deny rule it no longer has. The
    Claude Code settings.json denies .decanter.json, .env and
    push --force — but the scaffolded CLAUDE.md and opencode.json still
    claimed *.remote.js was blocked too. Those conflict artifacts were removed
    in the MCP pivot; both files now describe the policy that actually ships.

  • Docs: mcp connect / mcp serve no 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 what init
    wrote to .env / .decanter-auth.json, and can at most refresh an OAuth
    token. The mcp-connect / mcp-serve pages and the template AGENTS.md now say
    so outright — obtaining credentials is exclusively init's job, and a
    "no MCP credentials" answer means run init, 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 paste N8N_MCP_TOKEN into a file and
    stops there, leaving no config, template, .gitignore or agent wiring behind.
    The error now says the dir is not a sync dir yet, that .env alone 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 skip init — 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
    guarded n8n-instance server), permission rules and hooks are all read at
    agent startup, and init normally 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.md tells the agent to ask for a restart when the n8n-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
    renameNode went unreported until a later push refused 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.json matched 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-decanter on PATH, 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/.bin and falls back to PATH.
  • "decanter.config.json not found" no longer sends you to init when 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_DIR form
    that reaches it. When there really is no sync dir, the init advice is
    unchanged.

Added

  • The routing check also looks at parent directories, up to your repository
    root.
    Agents merge .mcp.json from 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> (or N8N_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 entry init writes into the sync dir is invisible from up
    there, and hoisting it to the repo root spawned the guard where no
    decanter.config.json could 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. init does not take --dir — it still takes the
    directory to scaffold as an argument.

  • init now tells you how to wire an agent when your sync dir is nested in a
    bigger project.
    Agents look for .mcp.json, opencode.json and
    .claude/settings.json from the directory they were started in and never in
    one below it, so everything init scaffolds is inert for an agent started at
    the repo root above the sync dir. When init sees a project around it (a .git or
    package.json in 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 .env and quietly
    stop protecting your credentials. init prints 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

Choose a tag to compare

@buttjer buttjer released this 17 Aug 08:36
4c18c61

Added

  • The live mirror now tells the agent when it overwrote your work. It runs a
    full pull after 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 transport init scaffolds). mcp serve pipes
    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.

  • init now says that its permission rules only bind the next session. It
    writes .claude/settings.json with the deny rules that keep an agent off
    .decanter.json, .env and push --force — but agents read permission config
    at startup, not on change, and init is 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 run fixtures 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. input takes
    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 --simulate now 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. The simulate check names the splits in its details
    (terminal and --json). test still replays main[0] only — n8n's
    pinData is one flat items array per node, with no output dimension — and
    scenario check now spells out which of the two you are looking at.

  • test now 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 run success and
    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 check warns about what the replay will throw away. Both replay
    paths (test's pinData, 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 .ts extension in a node file's import no longer fails the
    typecheck.
    The scaffolded tsconfig.json now sets
    allowImportingTsExtensions, so import { total } from "../../../shared/money.ts" type-checks — until now it was rejected
    (TS5097) even though push bundles 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
    updated tsconfig.json on the next init — a tsconfig.json you edited
    yourself is reported as drift and left alone, so add the option by hand
    there.
  • Two of the four import rules for .ts nodes 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's layout
    details, push, node run) and exactly once per push;
    preflight --fail-on=warn is the strict variant for CI. Node builtins
    and npm packages not opted into bundleDependencies still 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.mjs session 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 an n8n server added
    with claude 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-run init in an
    existing sync dir (or re-copy the hook from the template) to pick it up.
  • The scaffolded tsconfig.json now covers the whole sync dir, not just
    shared/ and workflows/ — 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, widen include to
    ["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-level return outside any workflow's code/) is now part of the
    program and reports TS1108 — move it into a workflow or add its folder to
    exclude; and when init scaffolds into an existing project that had
    no tsconfig.json, the new config sweeps that project's own .ts/.js
    into the node-file typecheck — add your app dirs to exclude if they
    shouldn't gate pushes.

Fixed

  • diff and preflight no longer report a CONFLICT for 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, and push has 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 rules init scaffolds. Such a node now reads as
    push pending, which is what push does with it. pull no longer warns
    CONFLICT for the same case on .ts nodes.
  • A .js.ts conversion no longer reads as data loss. Re-pointing a
    node's //@file: placeholder is the sanctioned way to convert, and push
    and pull both adopt it before doing anything — but diff and preflight
    looked the file up in .decanter.json alone and announced local file code/<node>.js missing for the file you had just replaced. They now read the
    placeholder too, so a converted node reports local changes in code/<node>.ts — push pending, identically for every converted node.
  • preflight no longer prints ✓ parity local code matches the draft
    directly above ✗ drift CONFLICT.
    Both checks read the same facts, so
    parity may claim a match only when every node is in sync; divergence that
    drift owns is reported as an info line pointing at it.
  • mcp connect survives an unreachable n8n instead of dying at the
    handshake.
    initialize was forwarded like any other message, so a
    connection failure answered the handshake with an error: the agent's MCP
    client got no serverInfo and 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...
Read more

v0.10.0

Choose a tag to compare

@buttjer buttjer released this 08 Aug 04:43
41f7870

Added

  • scenario create <workflow> "<slug>" --extend — top an existing
    scenario up with the pinnable nodes it is missing, keeping every value already
    authored. Previously scenario create refused an existing file outright, so a
    scenario test rejected 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 --scaffold now works with no instance. The fill entries
    were always built from your local workflow.json; the instance only supplied
    the per-node output JSON Schemas, which annotate the fill rather than
    enable it. With no N8N_HOST configured it now says the annotations are
    missing and scaffolds anyway — each node lands as provenance authored
    instead of scaffolded, so the difference stays visible in the file. This is
    what makes preflight --offline --simulate reachable 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.md states which of the four need the instance.

Changed

  • Breaking: a slug-less scenario create --scaffold now writes
    scenarios/scaffold.json, not scenarios/scenario.json.
    The old default collided with the scenario
    verb, and the flag parser refuses to read a verb name as a flag value — so
    preflight --simulate --scenario scenario failed 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 check now reports the test gate too, not just the
    preflight --simulate one.
    The two demand different node sets on purpose —
    --simulate asks only for nodes the capture reached, test asks 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 by test. check now says which gate you have passed, and
    names --extend as the way to close the difference. The docs that asserted the
    two rules were the same are corrected.

  • scenario create --execution pins 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 with test straight 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), so N8N_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 --host on
    its own names --token when 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 typescript installs 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 and preflight's unlock now both say
    npm i -D typescript@^5 and name the reason. init's scaffold already pinned
    ^5; this only ever bit projects init deliberately left alone.

  • A 403 from the public API now names the scope you are missing. n8n answers
    a valid-but-under-scoped N8N_API_KEY with 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:read does not cover /columns or /rows, which need
    dataTableColumn:read and dataTableRow:read. The data-table hints also say
    outright that decanter only ever reads them, so no write scope is needed.

  • scenario create prints 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 next pull or
    push swept 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 in fill, yet it said "see the
    _decanterScenario block"; 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 "run n8n-decanter init", and a blind field-test round watched an
    agent conclude from it that there was no .env and no token at all — then send
    its user through a pointless init. The .env existed; 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 offers init only as the OAuth alternative. It
    also maps 403 now, pointing at n8n → Settings → MCP.

  • A missing typescript is reported as a skipped check, not a failed one.
    preflight's node-file typecheck needs typescript in your project. A
    globally installed decanter ships none (it is a devDependency), and init
    leaves an existing package.json alone — 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.md tells agents that .env is unreadable by policy
    and that this is not evidence it is missing
    — the reasoning trap behind the
    401 finding above.

  • node run no 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 — an IF's false branch, a Switch'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 with test) — the same signpost pattern $vars and
    $secrets already use. n8n-globals.d.ts declared the parameter and
    docs/cli/node-run.md listed the calls as fully covered, so both surfaces had
    promised something the emulation never did.

  • pull no longer destroys uncommitted local edits. It committed the folder
    after overwriting it, so an uncommitted .js edit 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
    (watch and 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.json yet, 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 blocks jsCode, so the remote body is empty), writes the
    source into code/<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, matching watch.

  • Scenario gaps are now judged per branch, not per node. A branching node
    (an IF, a Switch) emits on one output per run, but preflight --simulate and scenario create --execution read its first output for every
    outgoing edge. Two consequences, b...

Read more

v0.9.0

Choose a tag to compare

@buttjer buttjer released this 04 Aug 20:05
013505b

Changed

  • The agent guard now refuses a publish_workflow that would take a broken
    draft live.
    publish already 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 connect and
    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.X and $items('X').
    Previously only the first was detected, so a rename could strand a $node[…]
    call site that nothing reported: preflight, push, test and publish all
    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
    --force does 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 under executions/ 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. Bare test is 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 — a test that 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.

  • publish refuses a draft carrying a dangling $('…') reference.
    Previously nothing checked: the compliance guard runs on push, preflight
    and backup — not publish — 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 read publish already makes), not your local folder: workflow.json is
    a snapshot, so grading it would pass a broken workflow on a stale mirror and
    block a legitimate publish from a fresh clone.

Added

  • init now scaffolds a hook that catches stranded $('…') references right
    after a rename.
    n8n's renameNode MCP op rewrites the node name and the
    connections only, so the references it leaves behind used to surface at the
    next push — arbitrarily far from the rename that caused them. On Claude Code
    a PostToolUse hook on update_workflow now 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, then push.

    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
    preflight would report clean. Silent when nothing references the renamed
    node. The checklist in the scaffolded AGENTS.md remains the contract for
    every agent — the hook is a reminder, not a replacement.

Fixed

  • Corrected the rename guidance: n8n's renameNode MCP 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; pull faithfully
    mirrors what n8n stored.

    init's AGENTS.md/CLAUDE.md now describe the real contract, including the
    repair order that matters: fix other nodes' expression parameters over MCP
    first, then local code, then push.
    The other order loses the code fix,
    because a forwarded MCP write schedules a background snapshot refresh whose
    pull overwrites unpushed .js edits.

  • 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 to workflow.json being hand-edited — turning the check green while
    n8n stayed broken.

v0.8.0

Choose a tag to compare

@buttjer buttjer released this 28 Jul 05:10
fc572ae

Added

  • The interactive picker offers a --force retry when a push hits the drift
    guard.
    Previously the error was printed and you were dropped back at the
    menu, having to leave the picker and re-run push --force by hand — even
    though the CLI had just told you --force would 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 than y/yes,
    returns to the menu exactly as before. Answering y re-runs the same menu row
    (flags included) with --force, overwriting the n8n draft only — the
    published version is untouched.

    It only appears for failures --force can 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 --force hint and exit non-zero, unchanged.

  • diff — the new verb for "show me the actual changed lines". It is the
    promoted half of status --diff: per-node unified line diffs of your local
    code against the n8n draft, .ts compiled first (bundling shared/*, 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 like pull/push;
    no workflow on a terminal opens the picker.

    It always exits 0. diff is an inspection view, like git diff — the
    gate is preflight. 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 old simulate verb, 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 reports simulate as skipped under --viewer
    ("a preview, not a pass/fail check"), never as a pass.

  • preflight --no-typecheck skips the types check — the escape hatch the
    retired check verb had.

  • The agent guard now logs a startup line and an audit trail (mcp connect
    and mcp serve alike, on stderr):

    guard: connected to <host> — forwarding all n8n MCP tools, blocking jsCode writes in update_workflow
    guard: forwarded search_workflows
    

    Previously 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 preflight finding can now carry details[] — the full list behind
    the one-line message: every layout violation, every tsc error, the
    drifted node names, the viewer URL. Printed indented under the check line,
    and present in --json. This is how the information check printed in full
    survives its removal; without it, folding check into preflight would 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 fresh git clone
    everything looks equally recent and the list falls back to alphabetical until
    your first pull or push. Scripted list output is deliberately unchanged.

  • The CLI banner's n8n wordmark 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 and NO_COLOR stay plain,
    exactly as before.

  • Breaking: preflight's profiles are replaced by two orthogonal flags.
    --full and the Profile model 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 instance
    

    Migration: --full--simulate. And read the next entry carefully —
    --offline still exists but means something narrower.

  • Breaking: preflight --offline no 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
    on preflight --offline simply stops running the engine and quietly loses
    that coverage. Migration: preflight --offline --simulate is the old
    --offline. (This narrowing is also what makes --offline fast enough for
    the per-edit hook: it now spawns no Docker container.)

  • Breaking: preflight --json replaces profile with flags. 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 in checks[] gains an optional
    details: string[].

  • preflight with no workflow and an empty "workflows" config now checks
    every pulled workflow
    instead of erroring with "no workflow ids" — the
    behaviour the check verb had, kept now that preflight absorbs it.

  • A workflow folder with an unreadable .decanter.json no longer fails your
    gate.
    check scanned folders, so a corrupt state file anywhere under
    workflows/ was a hard error for the whole run. preflight grades 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 --simulate accepts multiple workflows. The old simulate
    verb took exactly one; preflight loops, so a multi-ref run spins one engine
    container per workflow, serially.

  • The scaffolded template now runs preflight --offline where it ran
    check
    — the PostToolUse verify hook, both package.json scripts, and the
    agent-facing prose in AGENTS.md / CLAUDE.md. Existing sync dirs keep
    their files
    : re-run n8n-decanter init to be offered the refresh, and note
    that init leaves locally-modified files alone, so a hand-edited hook or
    package.json still invokes a removed verb until you update it yourself.

  • The scaffolded agent permission allowlist swaps its check/status/
    simulate rules for a diff pair (both the bare and npx shapes);
    preflight --simulate is already covered by the existing preflight:* 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 push as part of finishing the
    work, and reserves "ask the user first" for publish.
    A push lands on the
    workflow's draft and never changes what is running; only publish /
    push --publish / unpublish do. 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. Affects template/AGENTS.md.example (copied into new sync dirs by
    init) and the /docs/agents surfaces. (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 → publish flow. preflight is local-only (it no longer runs on the instance),
    so test — which runs the workflow's draft — is only meaningful after a
    push. The old contract framed test as a pre-push runtime check and ended its
    loop at preflight → push; both are now reconciled to the new order. Affects
    template/AGENTS.md.example and the /docs/agents surfaces. (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: preflight no longer runs the instance-side test stage. It
    ran test_workflow against 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 -10 parity warn.
    A report could read caution, 90/100 while its runtime evidence was about
    code you weren't shipping. preflight now grades ...

Read more

v0.7.0

Choose a tag to compare

@buttjer buttjer released this 24 Jul 13:09
9f3a78a

Added

  • check now warns when local work has not been registered with n8n — 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. It stays a warning, not an error: push reconciles the file map, and
    the compliance guard runs before that reconcile, so failing here would refuse
    the one command that fixes it.

  • n8n-decanter --version prints the installed version (-v too), 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 init points 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.)

  • init can run non-interactively via --host / --token / --api-key.
    Passing any of them drives setup purely from the flags plus any existing
    .env and 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). --host is required in this mode
    (a scheme-less local host is normalized to http://, like a typed one) and
    wins over an existing .env value; --token sets N8N_MCP_TOKEN (headless
    OAuth is still terminal-only); --api-key sets the optional N8N_API_KEY.
    The flag-less path (interactive, or piped answers) is unchanged. (Plan 35
    field-test finding.)

  • node run now emulates $jmespath. A Code node that calls
    $jmespath(data, expr) (or the $jmesPath alias) runs offline, matching
    n8n's result (backed by jmespath@0.16.0, the version n8n pins). It also
    fills in $items()/$node (views over the fixture's nodes), $vars/
    $secrets (new fixture fields), and $nodeId/$nodeVersion/$webhookId.

  • node run fixtures gained vars and secrets to pin the instance-scoped
    $vars/$secrets when a node reads them.

Changed

  • Breaking: the scaffolded Claude Code settings moved from
    .claude/settings.local.json to .claude/settings.json, and init now
    migrates existing sync dirs. The file holds project policy — decanter's verb
    permissions plus the verify.mjs and mcp-route-check.mjs hooks — with
    nothing machine-specific in it; it was already being committed and tracked in
    the shared .decanter-template.json, so local was 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
    deny beats an allow, 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) — init says what to move, and
    --force resolves it by removing the old file. A settings.local.json that
    init never wrote is never touched. (Plan 56.)

  • Breaking: backup restore takes 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 of backup list you have to
    hand. backup restore order-sync 2026-07-24 and backup restore order-sync a1b2c3d4 both 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 bare OK. check
    is offline by definition, so green means "well-formed", never "live in n8n".
    The agent guide gained the matching rule, and the .js.ts recipe now ends
    at push rather than check — 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 green check
    as "done", and never pushed.)

  • scenario create / scenario check with 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 run signposts instead of crashing on instance-scoped globals. A
    global whose value lives on the running instance ($vars/$secrets when
    unpinned, $evaluateExpression) now throws a friendly message that names the
    global and points to test (or the fixture field) — never a bare
    ReferenceError. docs/cli/node-run.md documents the covered / partial /
    unsupported boundary.

Fixed

  • The documented Claude Code skills-install commands are no longer
    copy-paste-broken.
    /plugin marketplace add / /plugin install are
    in-session slash commands, but the docs and the scaffolded AGENTS.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.ts conversion 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
    pull landing in the window before the first TS push — notably the
    on-by-default live-mirror background refresh after a structure edit — rewrote
    the placeholder back to .js and left .decanter.json pointing at the
    deleted .js file, so the next push failed with referenced node file missing. Pull now honors the re-pointed placeholder exactly as push does
    (they share one reconcile step). (Plan 35 field-test finding.)

  • init no longer breaks local http instances. A scheme-less host typed
    at the n8n host: prompt now defaults to http:// for local addresses
    (localhost, loopback, private LAN ranges, *.local) and https://
    otherwise. Previously every scheme-less host got https://, so a local n8n
    (plain http) was written to .env as a TLS URL and every sync/guard call
    failed with fetch failed. A scheme you type is still kept as-is.

  • n8n-globals.d.ts no 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-only preflight gate, so an agent following the
    template's recommended edit → check → preflight → push loop no longer stalls
    on a permission prompt at the gate itself. Also dropped the obsolete
    *.remote.js deny rule — those artifacts were removed in the Plan 32 MCP pivot.

v0.6.0

Choose a tag to compare

@buttjer buttjer released this 23 Jul 14:17
7995d22

Added

  • backup — git-native, redeployable disaster recovery. n8n-decanter backup create <workflow> captures the workflow's full REST export into a
    committed, versioned workflows/<slug>/backups/<timestamp>.<versionId>.json
    store — the fidelity MCP can't give (credential refs + description kept;
    pinData/staticData stripped; each Code node's jsCode stays a //@file:
    placeholder, so no code is duplicated). It dedupes on an unchanged
    versionId and rolling-prunes the working set to backupLimit (config,
    default 20; 0 keeps all). backup restore <workflow> [--version <id> | --at <ts>] re-inlines the Code from code/ 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: needs N8N_API_KEY. The backup file is
    not auto-committed (it carries credential refs and any embedded
    secrets) — review it, then git add deliberately.
  • Live workflow.json mirror — the review snapshot refreshes itself after
    an agent restructures a workflow through the guard.
    When a structure edit
    is forwarded through mcp connect / mcp serve (a non-blocked
    update_workflow), decanter now schedules a debounced background pull of
    that workflow, so the read-only workflow.json (+ code files + state) stays
    fresh with no manual pull. On by default; set "liveMirror": false in
    decanter.config.json to 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
    changes mcp connect/serve default 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 when N8N_API_KEY is set (--no-fetch opts
    out) and reading production run health (history, via MCP
    search_executions or 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=warn promotes a caution to exit 1; --fail-fast
    stops at the first failure. --json emits the full report (stable check
    ids + remediation strings — the agent contract). preflight never
    mutates
    in any profile: no push, publish, restore, or draft write —
    test runs in a never-mutate mode and simulate headless with
    --network-none forced on. The single gate to run before push/publish.
  • test — instance-side pinned test runs (the recommended runtime
    check).
    n8n-decanter test <workflow> runs the workflow on your
    instance via MCP test_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; --trigger picks the start node,
    --json emits the report). The run targets the draft — the live
    version is never affected. On a terminal, when local code differs from
    the draft, test offers 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. simulate stays the offline sibling —
    pre-push/CI/isolation/version-rehearsal — and its docs now recommend
    test first.
  • mcp connect — the stdio MCP guard, auto-wired by init. The default
    way a coding agent reaches your instance's MCP server: the scaffolded
    .mcp.json (and opencode.json) carry a static, secret-free
    n8n-instance entry ({"command":"n8n-decanter","args":["mcp","connect"]}),
    so guarded instance access exists the moment init runs — nothing to
    start, no secret to manage (stdio pipes are private). Decanter holds the
    credentials; the same guard rule as mcp serve applies (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 + push flow. 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_workflow calls that write Code-node source, via either
    a jsCode key or a setNodeParameter op whose path targets jsCode,
    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.mjs session hook that nudges agents whose MCP config
    still points at the instance directly, and the sync-dir AGENTS.md
    contract is now guard-first.

Removed

  • Breaking: the structure/lifecycle verbs are gone — rename, create,
    node create (and its --ts flag), and node rename.
    Those acts go
    through n8n itself: the n8n editor, or n8n's MCP tools reached through
    the new mcp connect/mcp serve guard (which is exactly what the
    official n8n skills drive). Decanter's job is the reconcile: the next
    pull re-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 no jsCode (the guard blocks code in
    addNode) — it now lands as an empty file whose first push seeds
    the source, completing the guarded authoring loop. Two behaviors did not
    survive the removal: $('…') refs inside local .ts sources 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_workflow first, as the n8n skills teach).
  • Breaking: the delete verb 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 duplicate verb 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 and pull the copy.
  • Breaking: watch's browser-reload proxy is gone — browserReload and
    proxyPort config 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). watch now just
    prints the editor deep link with a note to keep the tab open; it updates
    live on every push.
  • Breaking: simulate --pin and per-node fixtures/ are gone — folded into
    scenario.
    The per-node workflows/<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 now scenario create --execution <id>. A
    leftover fixtures/ dir is a hard error from simulate/check naming
    the replacement; there is no silent read-path or auto-migration for it
    (unlike a leftover mocks/ dir, which auto-migrates to scenarios/ on
    first touch — see the scenario namespace 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 watch plus
    a manual push sharing .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...
Read more

v0.5.0

Choose a tag to compare

@buttjer buttjer released this 21 Jul 15:58
61c378b

[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 push runs status on the workflow named push. Flags
    may still appear in any position.
  • Breaking: node operations moved under a node namespace. add
    node create <workflow> "<Node name>", the two-name node rename →
    node rename <workflow> "<old node>" "<new node>", and run <node-file>
    node run <node-file>.
  • New workflow folders are kebab-case (Order Syncworkflows/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-tables verb — a read-only fetch of n8n data-table schemas and
    rows (the built-in project-scoped tables, n8n ≥ 2.x) into a top-level,
    gitignored data-tables/<table>/{meta,columns,rows}.json dir, for developing
    and debugging against real table contents offline. --filter '<json>',
    --search, and --sort pull only a slice of a large table server-side (the
    applied filter is recorded in each table's meta.json); --limit/--all
    control page size and exhaustion. It never writes a data table.
    data-tables clean removes the dir (offline). Gated by the new dataTables
    config key (default true); 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.json now caches the workflow's display name (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 if workflow.json is
    missing or corrupt.
  • list --json emits [{ name, id, dir }] for tooling (remote-only
    workflows under --remote have dir: 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.
  • simulate now replays single-iteration loops. A workflow whose only
    repeated node is a splitInBatches ("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 --workflow flag. Workflow rename is now the single
    top-level form rename <workflow> "<new name>"; node rename lives under
    node rename.