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 --pushA 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:
- the edit goes into the generator;
- the generator pushes the workflow;
- 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