All posts

How to keep n8n workflows in code: a generator instead of clicks in the editor

Six months on, nobody remembers why a node is set up the way it is, and the copies of one workflow across different accounts have already drifted apart. The editor is no help here.

ONE EDIT — TWO VERSION DIFFS RANDOM IDS - id: 8f3a92e1 + id: 07c4d5aa - id: 66b0c1f9 + id: 91e2a3bd - id: c52f70de all nodes changed — edit invisible ID = MD5(NODE NAME) id: a3c92f10 - cron: 0 6 * * * + cron: 0 7 * * * id: b7e01d55 id: c4f00e21 exactly the edit you made
Short answer

Describe the workflow with a script that builds the JSON, and push it with PUT /api/v1/workflows/{id}. Make node ids deterministic — a hash of the name — so a repeat push does not break the connections or invent changes that were never made. One generator with a profile per account serves any number of clients: the fix goes in once, and a run updates every copy. After that you cannot edit in the editor: the next push overwrites it.

Sections · 9

Building an n8n workflow with the mouse is fine while there is only one of them and you still remember how it works. After that you hit two problems the editor does not solve.

The first: six months on, nobody can say why the filter is set up the way it is. The free version of n8n has no change history — there is nothing to compare against, and whoever built it does not remember either.

The second: several clients need the same workflow. You copy it for each of them, then find a bug and fix it in every copy separately. By the third copy they have already drifted apart, and you cannot see it.

The answer is the same as for any other code: describe the workflow as text, keep the text in a repository, and let a machine push it.

#What exactly gets described in code

Not all of n8n — one workflow. The script builds the same JSON the editor hands you on export: the list of nodes, their parameters, and the connections between them.

const node = (name, type, params, pos) => ({
  id: nodeId(name),
  name, type, position: pos,
  parameters: params,
  typeVersion: 1,
})

From there it is ordinary code: loops, conditions, functions. A piece of logic is written once and reused. Nodes of the same shape are not drawn with the mouse; they are generated.

The result is pushed through the public API:

curl -X PUT "$BASE/api/v1/workflows/$ID" \
     -H "X-N8N-API-KEY: $KEY" \
     -H 'Content-Type: application/json' \
     --data @workflow.json

#Why node ids have to be deterministic

This is the detail the whole approach rests on.

If the generator hands out fresh random ids every time, each push looks like a full rebuild of the workflow: every connection has “changed”, even though the logic is the same. Comparing two versions becomes impossible, and there is no longer any point in keeping them in a repository.

One line fixes it — the id becomes a hash of the node name:

import { createHash } from 'node:crypto'

const nodeId = (name) => createHash('md5').update(name).digest('hex')

Now a node with the same name always has the same id. A repeat push leaves the connections alone, and the difference between versions shows exactly what you changed.

#How to keep one workflow across several accounts

This is where the approach pays for itself.

Everything that differs between clients moves into profiles: credentials, the webhook path, the workflow name, the pipeline id. The logic stays shared.

const ACCOUNTS = {
  first:  { creds: 'crm-first',  hook: '…', pipeline: 'PIPELINE_A' },
  second: { creds: 'crm-second', hook: '…', pipeline: 'PIPELINE_B' },
}

Running it with a profile key builds and pushes the right copy:

node build-flow.js --account=second --push

A bug is fixed once in the generator, and a run updates every copy. There is nothing left to sync by hand — and no way to forget to do it.

#How to check the logic with tests

When the workflow is code, you can run its behaviour against examples without touching the live instance.

We pull the pure logic — parsing the input, choosing a branch, assembling the request body — out into separate functions and cover them with test cases: record created, record moved, cancelled, reopened, fields empty. On a workflow of middling complexity that comes to more than a dozen cases.

This is not a substitute for checking against live data. It is a way to stop hunting the same bugs by hand after every edit.

#Why the editor becomes read-only after this

Once you are on a generator, the n8n editor is for looking and for running things by hand. Not for editing.

Any change made with the mouse disappears at the next push. The order is always the same, with no exceptions:

  1. the edit goes into the generator;
  2. the generator pushes the workflow;
  3. the result is checked in the executions.

#When not to do this

The approach has a price: somebody has to write the generator, and for one simple workflow that is overkill.

It earns its keep when at least one of these is true:

  • more than one client or account needs the same workflow;
  • the workflow runs to more than a dozen nodes and will keep changing;
  • you need to see exactly what changed between versions;
  • money depends on the workflow, and a breakage nobody notices is unacceptable.

For a one-off “put the form into a spreadsheet”, the editor is enough.

#Takeaways

  • The workflow is described by a script that builds the JSON, and pushed with PUT.
  • Node ids are a hash of the name; otherwise every push looks like a full rebuild.
  • Account profiles give you one workflow for any number of clients.
  • Pure logic can be covered by tests instead of rechecked by hand after every edit.
  • After the switch, the editor is read-only. Edits made there disappear at the next push.

#Common questions

#Can you combine the generator with manual edits?

Technically yes, in practice no. Any manual change lives until the next push. If you did have to patch something urgently, move it into the generator the same day.

#What happens to the credentials?

They stay in n8n. The generator only deals with their ids, so no secret is ever stored in the repository.

#Are there ready-made tools instead of your own generator?

There is the built-in JSON export and import, plus a few third-party wrappers around the public API. They move a workflow as it is. The moment a second account needs the same logic, you need a layer that fills in the differences — and that layer is the generator.

#Sources

  • n8n Public API — the methods for reading and updating workflows
  • From our own experience
weirflow — Business process automation. Published: August 21, 2026

Describe your situation

Tell us which process eats the most time. We will write back with what can be automated and roughly what it would cost.

Discuss a project
All posts