Free your Code nodes · Malte ButtjerMalte Buttjer · 5 August 2026 · Updated 21 August 2026<br>Free your Code nodes<br>A guard proxy for AI agents editing n8n workflows: Code node source lives in git, one write is blocked<br>Point a coding agent at n8n's built-in MCP server and it genuinely works, right up until it edits a Code node. This is the guard proxy I built so it can keep everything except that one write.<br>Normalize orderScore risk{ }{ }normalize-order.tsscore-risk.tsone file per Code node — named after it
n8n is the best thing that happened to the boring half of my work. Hundreds of<br>integrations somebody else keeps current. Credentials in a vault. An execution<br>history that knows which item failed, on which node, and will retry exactly<br>that one. A canvas a colleague can read without opening an editor. And all of<br>it self-hostable if you want it, with no lock-in.
Recent versions also ship a built-in MCP server, further than most tools have<br>gone. Point a coding agent at it and it genuinely works: search workflows, read<br>their structure, add nodes, rename them, wire connections, publish. For the<br>first ten minutes it feels like the future arrived on schedule.
Then I handed it a real job. Our Amazon and eBay feeds were built by two<br>sprawling Code nodes, each walking Shopify’s GraphQL its own way, and I wanted<br>them consolidated onto shared types. The agent managed it, and the diff for a<br>few hundred rewritten lines was two changed strings. That was the moment I<br>noticed what I’d given up.
A Code node is a string inside a JSON blob
In n8n, a Code node’s source lives in a jsCode field: a string, inside a<br>node object, inside a workflow document. A perfectly reasonable way for a<br>workflow engine to store it. A terrible way to own a few hundred lines of<br>business logic.
Every tool you’d normally point at code stops at that boundary:
No diff worth reading. A rewritten node is one changed string; a 40-line<br>logic change diffs about like a node nudged 20 pixels.
No types. Nothing checks that $input.first().json.customerId exists, or<br>that the helper you pasted into four nodes still agrees with itself.
No sharing. Common logic is duplicated per node; a shared module has<br>nowhere to live.
No review surface. The change is invisible until it runs.
The problem was never that agents write bad code. It’s that they write<br>unreviewable code, quickly.
Two fixes that don’t work
Take MCP away. Now you’ve discarded the part that was working. Structural<br>edits (add a node, rewire a branch, wire the error path) are tedious by hand,<br>well-suited to an agent, and validated by the engine itself. A real capability<br>traded for a blunt one.
Mirror the whole JSON into git. The usual “n8n as code” answer, and it is<br>write-hostile: pushing a whole document back clobbers whatever moved on the<br>other side, and workflow JSON churns for reasons that aren’t yours: node<br>positions, version ids, credential references. You get a history of noise<br>around occasional signal, and a sync direction you don’t quite trust.
Both fixes draw the boundary around the product: all of n8n, or none of it.
Split ownership at the sharp edge
The observation everything else follows from: structure and code want<br>different owners.
Structure is n8n’s job, and n8n is good at it; that’s the premise. Rebuilding<br>the canvas, the validation, the execution model in git means writing a worse<br>engine to get better diffs. Code is git’s job: files, types, review, blame,<br>history. n8n never claimed otherwise: storing a function as a string is the<br>correct call for a workflow engine; the mistake was letting that string become<br>the only place the code lives.
So don’t take MCP away from the agent. Take one write off it.<br>n8n-decanter, my own project,<br>sits between agent and n8n as an MCP server of its own,<br>forwards the entire tool surface (create, read, update, rename, connect) and<br>refuses one write: the update_workflow that sets jsCode. The agent connects<br>through a scaffolded .mcp.json, never holds a second set of credentials, and<br>sees n8n’s full toolset minus one door. The structural write sails through; the<br>source write comes back with an address:
coding agent — Order enrichment
(The guard also inspects publish_workflow before forwarding it; more under<br>Being wrong should be cheap.)
What it writes instead
A folder per workflow, one file per Code node:
workflows/<br>amazon-marketplace-sync/<br>workflow.json # read-only mirror of the structure<br>.decanter.json # node id → file, per-node sync hashes<br>code/<br>amazon-feed.ts<br>state-sanitize.js<br>code-in-java-script4.js # the file a node nobody renamed gets<br>ebay-sync/<br>code/<br>ebay-feed.ts<br>shared/<br>compat.ts # imported by both feeds, bundled in at push<br>types/shopify.d.ts # one set of Shopify types for every node<br>workflow.json is a snapshot, never a source of truth; each Code node’s<br>source is replaced by a pointer:
"name": "Amazon feed",<br>"type": "n8n-nodes-base.code",<br>"parameters": { "jsCode": "//@file:code/amazon-feed.ts" }<br>Structure still diffs cleanly in git;...