There is a category of work that is genuinely tedious to write and trivial to describe. A form submission goes to a spreadsheet, a notification, and a CRM. A daily export lands in a folder and needs parsing. A webhook from one service has to become a call to another with the fields renamed.
n8n does that well, and doing it in code means an endpoint, three API clients, credential storage, retry handling and a deployment — for something with no product logic in it at all.
It also, reliably, becomes the place where important logic ends up living without tests, without review and without anybody remembering it exists. This is where I draw the line, having been on both sides of it.
What is n8n actually good at?#
Connecting systems that each have an API, where the work is mapping fields and the value is in not writing the plumbing.
| Task | Why not code |
|---|---|
| Form to CRM to notification | Three integrations for zero product logic |
| Scheduled report pulled from an API into a sheet | A cron, a client and a parser you would maintain |
| Watching a folder or inbox for files | Polling infrastructure you do not want to own |
| Enriching a lead from a third-party service | One call, one mapping, no state |
| Internal alerting on a database condition | A query on a timer, and nobody wants a service for it |
The common thread is that all five are integration rather than logic. There is no branching that a business rule depends on, no state that matters, and no consequence to a run being repeated. Those are exactly the conditions under which a visual workflow is easier to build and easier to change than the equivalent code.
They also share a property that makes them safe to build quickly: if any of them fails, somebody notices and does the thing by hand. That is the real licence to skip tests.
The self-hosted part matters too. Because n8n runs on your own infrastructure, customer data flowing through it is not being handed to a third-party automation service — which is often the difference between "we can use this" and a conversation with a client about where their data goes.
Where does it stop being the right tool?#
Four signals, and any one of them means the work belongs in the codebase.
When a business rule lives inside it#
The moment a workflow decides something — which plan a customer gets, whether an order is flagged, how a price is calculated — it holds product logic. Product logic needs review, tests and a history of why it changed, and a canvas has none of those.
When it touches production data with side effects#
Reading is one thing. A workflow that writes to your application database, charges a card or sends customer-facing email is production code that was never reviewed and cannot be rolled back with a deploy.
When the branching gets deep#
Three or four nodes of conditional logic is readable. Fifteen is a program written in the worst possible editor — no search, no diff, no way to see it all at once. If you find yourself scrolling a canvas to follow a path, it should be a function.
When more than one person has to understand it#
A canvas is readable to whoever built it and considerably less so to anybody else. Code has conventions that transfer — a reviewer who has never seen this file still knows what a function signature and a test file mean. A workflow has whatever names the author happened to leave on the nodes, and half of them will say "HTTP Request1".
When it needs to be fast#
Workflow execution has overhead per node and is not built for latency-sensitive paths. Anything a user is waiting on belongs in your application; n8n is for work that happens between systems, not in front of somebody.
The failure I see most often is drift rather than a bad initial decision. A workflow starts as three nodes mapping a form to a CRM, and eighteen months later it has a branch for enterprise leads, a lookup against the database, and a rule about which region gets which owner. Nobody decided that; each addition was small.
How do you decide, in one question?#
Ask what happens if it silently stops running for a week.
- Somebody notices and does it manually. A workflow tool is fine. Internal reports, notifications, enrichment — the loss is inconvenience.
- Customers are affected but the data is recoverable. Borderline. Acceptable with real monitoring and an owner, and better in code if it is likely to grow.
- Money, data or customer trust is lost. It belongs in the application, with tests, review and error tracking.
It is a better question than "is this complicated" because complexity is a poor proxy for risk here. A workflow with twenty nodes assembling an internal weekly digest can break for a fortnight and the only cost is that somebody asks where the digest went. A workflow with two nodes that marks accounts as paid can break for an afternoon and produce a support queue.
That question works because it captures the thing that actually distinguishes the two — not complexity, but consequence. A complicated workflow producing an internal chart is fine. A three-node workflow that provisions accounts is not, however simple it looks.
How do you run it responsibly?#
Five practices, none of them optional once a workflow does anything that matters.
Export the workflows into the repository#
n8n workflows are JSON. Committing them means changes are diffable, reviewable and recoverable, and it means the automation is visible to anybody reading the codebase rather than hidden behind a login. Without this, the only copy of your automation logic is in a database on a server, which is also the only backup.
# Nightly, into the repo. A workflow you cannot diff is a workflow
# nobody can review or restore.
n8n export:workflow --backup --output=automation/workflows/ Committing them also gives you a diff when something changes unexpectedly, which is the only way to answer "did somebody edit this on Tuesday" after an automation starts behaving differently.
Give every workflow an owner and a description#
An instance with forty workflows and no notes is unmaintainable within a year. Name them for what they do, write two sentences on each about why it exists and what breaks if it stops, and record who is responsible. This is the same reasoning as a handover document — the information is cheap to write and impossible to reconstruct.
Alert on failure and on silence#
n8n can notify on an execution error. What it does not tell you about is the workflow that stopped being triggered — a webhook that was deleted upstream, a schedule that was disabled, a credential that expired quietly. That needs a dead-man switch, exactly as a cron job does.
Retention is worth a setting too. Execution history is useful for debugging and it accumulates payloads — which frequently contain customer data, sitting in a database that is probably less protected than your application's. Prune it on a schedule and shorten the window for anything handling personal information.
Keep credentials scoped#
A workflow tool accumulates access to everything it touches, and it becomes one of the most privileged systems you run. Give each integration a credential scoped to what that workflow needs, not an admin key that happened to be available, and rotate them like any other production secret.
Make executions idempotent#
Workflows retry, get triggered twice by a duplicate webhook, and get re-run manually while somebody debugs. Any step that creates or sends should check first, as with any job handler. A manual re-run that emails two hundred customers a second time is the classic version of this.
What does the hybrid look like?#
The arrangement I actually use: n8n triggers, your API decides.
Rather than building logic in the canvas, the workflow does the integration work — receive the webhook, poll the inbox, watch the schedule — and then calls one endpoint in your application with a clean payload. Everything with a rule in it is on your side of that line.
// One endpoint per automation. Validated, authenticated, tested,
// versioned, and reviewable — while n8n keeps the plumbing.
export async function POST(req: Request) {
if (req.headers.get('x-automation-key') !== env.AUTOMATION_KEY) return unauthorized();
const input = LeadSchema.parse(await req.json());
return Response.json(await handleInboundLead(input));
} This gets you the best of both. The tedious connective work stays in the tool that is good at it, and every decision the business depends on lives in a function with a test next to it. Changing a rule is a pull request; changing which systems are connected is a drag on a canvas.
Authenticate the call, always#
An endpoint that exists for an automation is still a public endpoint. A shared secret in a header is the minimum, and it should be a distinct credential per workflow so one can be rotated without breaking the others.
One endpoint per automation rather than one generic one is deliberate. A single /automation route taking an action name is a remote procedure call with no type safety, and it will grow a switch statement with eleven branches. Separate routes stay independently testable, independently authenticated and independently deletable when a workflow is retired.
Validate the payload#
Input arriving from a workflow tool has been through several transformations by nodes that will be edited by someone in a hurry. Parsing it with a schema at the boundary means a mis-mapped field is a clear error rather than a wrong record.
What about AI steps in a workflow?#
Fine for internal work, and I would not put one in a customer-facing path from a canvas.
Summarizing incoming support tickets for an internal digest, drafting a first-pass response for a human to edit, classifying leads into buckets — these are genuinely useful and the cost of an occasional bad output is low.
What does not belong there is anything a customer sees, because everything that makes a model reliable in production is missing. There is no evaluation harness, no prompt versioning, no cost logging, and no way to test a prompt change against past inputs before it goes live. A prompt edited in a text box and saved is a deploy with none of the safeguards.
The same hybrid applies: let the workflow gather the input and call your endpoint, where the prompt is versioned, the output is validated against a schema, and the token spend is recorded. This is the module argument — the model is a component in an application, and a canvas is not an application.
How does n8n compare to the alternatives?#
Four options in the same space, and they differ mostly on where your data goes and how much you are willing to operate.
| Option | Data location | Operational burden | Best when |
|---|---|---|---|
| n8n self-hosted | Your infrastructure | You update and back it up | Client data cannot leave, many integrations |
| n8n cloud | Their infrastructure | None | Same tool, no ops appetite |
| Zapier / Make | Their infrastructure | None | Few automations, non-technical owner |
| A cron job in your app | Your infrastructure | Already exists | Two or three automations, all in code |
The bottom row is the one worth taking seriously before adopting anything. If you already have a background worker and a scheduler, adding a task to it is genuinely less work than standing up a new system — and the logic lives with the tests and the review process instead of beside them.
The self-hosted argument is usually about data#
For a client whose customer records cannot pass through a third-party service, self-hosting is not a preference — it is the requirement that decides the tool. That is the most common reason I end up with n8n rather than a hosted equivalent, and it is worth stating explicitly in a proposal because it is a real constraint the alternatives fail.
The per-task pricing argument is usually overstated#
Hosted tools charge per execution and the arithmetic looks alarming at volume. In practice most automations run tens or hundreds of times a day, not thousands, and the hosting plus the maintenance of a self-hosted instance is rarely cheaper once your own time is counted honestly.
When should you replace a workflow with code?#
Four triggers, and hitting any of them is a signal rather than an emergency.
- A bug that took an hour to find. Debugging by clicking through execution history is slow. If that has happened twice, the logic wants to be in a file.
- A change nobody dares make. A workflow complex enough that people avoid touching it has become a liability regardless of whether it works.
- It needs a test. If you find yourself wanting to assert something about its behavior, it is application logic and should live where tests do.
- It became load-bearing. Something that started as a convenience and is now part of how the product functions has changed category, and the tooling should follow.
Do it as a replacement rather than a rewrite in place. Build the endpoint, point the workflow at it, delete the nodes it replaced, and keep the trigger. That way the automation never stops working and there is no moment where both versions are live and disagreeing.
The migration is usually smaller than expected, because most of the workflow was doing integration you keep. The part that moves is the decision-making in the middle, which is often forty lines and is the part carrying all the risk.
Automations become production code the moment somebody would notice them stopping. The tool does not change; what you owe it does.
What does this cost to run?#
Self-hosting is a small container and a database, so the infrastructure is close to free. The cost is attention.
An n8n instance is a system that needs updating, backing up and monitoring, and it holds credentials to everything it touches. That is real operational surface for something that often gets set up in an afternoon and then forgotten — and a forgotten instance with production credentials is a security problem rather than a convenience.
The honest counterweight is that for a solo developer or a small team, this is frequently still worth it. Five workflows saving an hour each per week is a good trade against a monthly update and a backup job. What is not worth it is thirty workflows nobody can account for, which is where an unmanaged instance ends up by default rather than by decision.
If you only have two or three automations, a cron job in your existing application is genuinely simpler — no new system, no new credentials, and the logic sits with everything else. The tool earns its place when the number of integrations makes writing each one by hand the slower path.
Conclusion#
Use n8n for integration and keep it out of your product logic. Mapping a form into three systems, pulling a scheduled report, watching an inbox — these are jobs where a canvas is genuinely faster to build and faster to change than the equivalent code, and there is no rule inside them worth reviewing.
Draw the line at consequence rather than complexity. Ask what happens if it silently stops for a week: inconvenience means a workflow is fine, and lost money, data or trust means it belongs in the application with tests and review.
Prefer the hybrid. Let the workflow do the connective work and then call a single authenticated, schema-validated endpoint in your codebase where every decision lives. Changing a rule becomes a pull request; changing which systems are wired together stays a drag on a canvas.
Whatever runs there, export the workflows into the repository so they can be diffed and restored, give each one an owner and two sentences of description, scope credentials per integration, make every execution idempotent, and alert on silence rather than only on failure — a workflow that stopped being triggered raises no error at all.
And keep AI steps to internal output. Nothing customer-facing should depend on a prompt edited in a text box with no versioning, no evaluation and no cost record. If you have automations that have quietly become load-bearing and want a view on which ones should move into the codebase, that is a useful hour.