n8n should not replace your entire backend

Limits of low-code tools like n8n: when to automate and when to write real software.

Cover for n8n should not replace your entire backend

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:

  1. Honeymoon phase. You set up a quick workflow, it works, everyone’s happy. “Look, no code needed.”
  2. Expansion phase. You start adding logic. Conditionals, loops, transformations. The workflow grows. It still works.
  3. 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:

  1. Identify the business logic inside the workflow. Usually it’s a Function node or a block of nodes with conditionals.
  2. Move that logic to a service (FastAPI if your stack is Python, Spring Boot if it’s JVM).
  3. 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

Aspectn8n workflowCustom API
Prototyping speedVery highMedium
Ease of maintenanceLow if there’s complex logicHigh with good practices
TestingNo integrated frameworkUnit tests, integration tests, CI/CD
DebuggingNode by node, visualLogs, stack traces, breakpoints
VersioningUnreadable JSON diffsClear git diffs
Code reviewVery difficultStandard
ScalabilityLimitedAccording to your design
Learning curveLow at firstMedium-high
IntegrationsHundreds of ready-made nodesYou implement them
Development costLow for simple flowsHigher initial investment
Tool dependencyHigh (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.

OshyTech

Backend and data engineering focused on scalable systems, automation, and AI.

Navigation

Copyright 2026 OshyTech. All Rights Reserved