n8n should not replace your entire backend
Limits of low-code tools like n8n: when to automate and when to write real software.

A few months ago I saw something that worried me. A team had built all their business logic inside n8n. I’m not talking about sending emails when someone signs up or syncing data between two services. I’m talking about order validation, price calculation with tiered discounts, shipping status management, and invoice generation. All in visual workflows with dozens of nodes, nested conditionals, and inline JavaScript functions. When something failed, nobody knew where to look. When they needed to change a business rule, they touched three different workflows and prayed they wouldn’t break the others.
n8n is an excellent tool for what it was designed for: automating integrations, connecting services, and executing flows that would otherwise be repetitive glue code. But it’s not a software development framework. And treating it as one has consequences that show up late, when they already hurt.
The problem isn’t n8n, it’s where you put it
I want to be clear from the start: I’m not against n8n or low-code in general. I use it daily for automations that save me hours of work. The problem appears when the tool becomes the entire system instead of being one piece of the system.
Low-code works best as glue between systems, not as the foundation of an application.
I’ve seen this pattern repeat in three phases:
- Honeymoon phase. You set up a quick workflow, it works, everyone’s happy. “Look, no code needed.”
- Expansion phase. You start adding logic. Conditionals, loops, transformations. The workflow grows. It still works.
- Pain phase. The workflow has 60 nodes. Nobody remembers why there’s an IF on node 23. A change in the external API breaks three branches. There are no tests. There are no clear logs. Debugging means clicking on each node to see what came out.
Decision rules: when n8n, when code
After using n8n for quite a while and getting it wrong more than once, I have a set of criteria I apply before deciding whether something goes in a workflow or in custom code.
It goes in n8n when…
- It’s glue code. Connecting service A to service B. Receiving a webhook and forwarding transformed data. Syncing a CRM with a database.
- The logic is linear or has few branches. If you can describe the flow in one sentence (“when an order arrives, send an email and update the CRM”), it probably fits in a workflow.
- It doesn’t need unit tests. If the logic is so simple that a test would be trivial, a workflow is enough.
- Maintenance can be done by someone who isn’t a developer. If you want marketing to be able to change the text of a notification without asking development.
- It’s a prototype. You’re validating an idea. If it works, you’ll extract it to code later.
It goes in code when…
- There’s complex business logic. Calculations with rules that depend on multiple conditions, states, configurations.
- You need tests. If a badly implemented rule can cause a business problem, you need automated tests. n8n has no integrated testing framework.
- There’s concurrency or state. Multiple processes accessing the same data, race conditions, transactions that must be atomic.
- The flow has more than 15-20 nodes with conditionals. This is a clear signal that complexity has outgrown what a visual workflow can manage readably.
- You need versioning and code review. n8n workflows can be exported as JSON, but diffing two 500-line JSONs in a PR is not the same as reviewing code.
Concrete examples
Example 1: New registration notification (n8n)
A user signs up on your app. You want to send a welcome email and notify the team via Slack.
This is textbook n8n. Linear flow, no complex logic, standard integrations.
Webhook (new user)
→ Send welcome email (SendGrid)
→ Notify Slack (#new-users)
→ Save to Google Sheets (onboarding log)There’s no point writing a service for this. n8n solves it in 10 minutes.
Example 2: Price calculation with discounts (custom API)
The final price of an order depends on: customer type, order volume, active promotions, loyalty discounts, taxes by region, and free shipping rules that change every month.
This should NOT be in n8n. The reasons:
# pricing_service.py - FastAPI
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from decimal import Decimal
app = FastAPI()
class OrderRequest(BaseModel):
customer_id: str
items: list[dict]
region: str
promo_code: str | None = None
class PricingResult(BaseModel):
subtotal: Decimal
discount: Decimal
tax: Decimal
shipping: Decimal
total: Decimal
rules_applied: list[str]
@app.post("/calculate-price", response_model=PricingResult)
def calculate_price(order: OrderRequest):
customer = get_customer(order.customer_id)
subtotal = sum_items(order.items)
# Discount rules
discount = Decimal("0")
rules = []
# Volume discount
if subtotal > Decimal("500"):
discount += subtotal * Decimal("0.05")
rules.append("volume_5pct")
# Loyalty discount
if customer.loyalty_tier == "gold":
discount += subtotal * Decimal("0.10")
rules.append("loyalty_gold_10pct")
# Active promotion
if order.promo_code:
promo_discount = apply_promo(order.promo_code, subtotal)
discount += promo_discount
rules.append(f"promo_{order.promo_code}")
# Taxes by region
tax = calculate_tax(subtotal - discount, order.region)
# Free shipping
shipping = Decimal("0") if (subtotal - discount) > Decimal("100") else Decimal("4.99")
if shipping == Decimal("0"):
rules.append("free_shipping")
return PricingResult(
subtotal=subtotal,
discount=discount,
tax=tax,
shipping=shipping,
total=subtotal - discount + tax + shipping,
rules_applied=rules,
)This has tests, versioning, logs, and can be reviewed in a PR. If a rule changes, you know exactly where to look.
Example 3: Inventory synchronization (depends)
Every hour you need to read inventory from an ERP and update a database that your app queries.
If the sync is straightforward (read, lightly transform, write), n8n works. If the sync has conflict rules, source priorities, or reconciliation logic, better a script or a service.
The risks of putting complex logic in workflows
Debugging
When an n8n workflow fails, the way to investigate is going node by node seeing what data came in and what data came out. There are no clear stack traces. No breakpoints. No structured logging. For a 5-node flow it’s manageable. For a 40-node flow with conditional branches, it’s a nightmare.
Testing
n8n has no testing framework. You can’t write unit tests for a node’s logic. You can’t mock external services. You can’t run a test suite in CI/CD before deploying a change. If your business logic is in n8n, it has no tests. Simple as that.
Versioning
Workflows are exported as JSON. You can put them in git, but:
- Diffs are huge and unreadable.
- There’s no merge tool that understands the structure of a workflow.
- A cosmetic change (moving a node’s position on the canvas) generates a diff that hides the actual change.
{
"nodes": [
{
"parameters": {
"functionCode": "const items = $input.all();\n// 47 líneas de JavaScript..."
},
"name": "Calculate Price",
"type": "n8n-nodes-base.function",
"position": [820, 340]
}
]
}Reviewing that in a PR is very different from reviewing a Python file with well-named functions.
People dependency
When logic is in code, any developer can understand it by reading the file. When it’s in a visual workflow, you need to open n8n, navigate through the nodes, understand the connections, and often interpret inline JavaScript written in a compact way. The learning curve for maintaining a complex workflow is steeper than it seems.
When to extract to a custom API
If you already have complex logic in n8n and you’re suffering from some of the problems I’ve described, the extraction pattern that works best for me is:
- Identify the business logic inside the workflow. Usually it’s a Function node or a block of nodes with conditionals.
- Move that logic to a service (FastAPI if your stack is Python, Spring Boot if it’s JVM).
- The n8n workflow calls your service via HTTP. n8n stays as the orchestrator, not as the logic engine.
Before:
Webhook → [40 nodes of logic in n8n] → Response
After:
Webhook → HTTP Request to your API → Response
(the logic is in your service, with tests and versioning)This pattern keeps the advantage of n8n (visual orchestration, easy integrations) without its drawbacks (debugging, testing, versioning of logic).
Comparison table: n8n workflow vs custom API
| Aspect | n8n workflow | Custom API |
|---|---|---|
| Prototyping speed | Very high | Medium |
| Ease of maintenance | Low if there’s complex logic | High with good practices |
| Testing | No integrated framework | Unit tests, integration tests, CI/CD |
| Debugging | Node by node, visual | Logs, stack traces, breakpoints |
| Versioning | Unreadable JSON diffs | Clear git diffs |
| Code review | Very difficult | Standard |
| Scalability | Limited | According to your design |
| Learning curve | Low at first | Medium-high |
| Integrations | Hundreds of ready-made nodes | You implement them |
| Development cost | Low for simple flows | Higher initial investment |
| Tool dependency | High (visual vendor lock-in) | Low (standard code) |
When to keep it in n8n
Not everything needs to be extracted to code. These are the tasks I keep in n8n without regret:
- Notifications. Emails, Slack, Telegram. Linear flows, no business logic.
- Simple data sync. Read from an API, transform a couple of fields, write to another.
- Pass-through webhooks. Receive an event and forward it to another service in a different format.
- Simple scheduled tasks. Every Monday at 9, export a CSV from the database and send it by email.
- Prototypes. Validate an integration idea before investing in code.
If you can describe what your workflow does in one sentence of fewer than 20 words, it’s probably fine in n8n. If you need a paragraph, it probably isn’t.
The pattern that works for me
My current architecture with n8n follows this principle: n8n orchestrates, code decides.
┌──────────────┐
│ n8n │
│ (orchestrator)│
└──────┬───────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌──────────┐
│ FastAPI │ │ External │ │ Databases│
│ (logic) │ │ services │ │ │
└────────────┘ └────────────┘ └──────────┘n8n receives events, schedules executions, and connects services. When there’s a business decision, it calls an API that has the logic, the tests, and the versioning it needs. The best of both worlds.
What I don’t want you to misunderstand
This article is not a critique of n8n. It’s a critique of using it for what it wasn’t designed for. It’s like using Excel as a database: it works until it doesn’t, and when it stops working it hurts a lot.
n8n is a fantastic tool in its domain. I’ve built automations with n8n that would have cost me days of development if I had written them in code. The key is knowing when you’re automating and when you’re programming. If you’re programming, use programming tools.
The next time you find yourself writing a 50-line JavaScript function inside an n8n Function node, stop for a moment and ask yourself: “Should this be here?”. If the answer isn’t an immediate yes, it’s probably time to open your IDE.


