DEV Community

Cover image for We rebuilt 47-node n8n flows until we admitted script-first AI workflows are just better
Lars Winstand
Lars Winstand

Posted on Originally published at standardcompute.com

We rebuilt 47-node n8n flows until we admitted script-first AI workflows are just better

We rebuilt 47-node n8n flows until we admitted script-first AI workflows are just better

At 2:07 a.m., an n8n run failed because GPT-5.4 returned one ugly JSON blob that didn’t match what the next node expected.

Not a big outage. Not a total failure.

Just one malformed field buried inside a 47-node flow with branches, retries, fallback prompts, webhook handling, and a couple of emergency fixes living in an n8n Code node.

So we did the usual thing:

  • opened execution history
  • clicked through branches one by one
  • checked whether the parser broke
  • checked whether retry logic fired
  • checked whether Claude Opus 4.6 behaved differently on the previous run
  • tried to remember why one branch existed at all

An hour later, we were still debugging a flow that was supposed to save time.

That was the moment we stopped pretending visual AI workflows scale cleanly.

My opinion now is pretty simple:

Use n8n, Make, and Zapier for orchestration. Put the AI decision layer in code.

If your workflow has real branching, retries, schema validation, provider switching, and tests, script-first wins.

The problem isn't n8n

I like n8n.

It's great for:

  • webhooks
  • cron jobs
  • Slack notifications
  • database writes
  • approvals
  • app-to-app glue

Same story with Make and Zapier. They are useful because they remove a lot of boring integration work.

The problem starts when your workflow tool becomes your application runtime.

That works fine for deterministic automations.

It gets ugly fast when the core logic depends on LLMs.

AI workflows break in ways normal automations don't

A normal automation is usually predictable enough that a visual graph stays readable.

A lead comes in. You enrich it. You write to a CRM. You send a message.

Done.

AI automations are different.

Now you need to handle things like:

  • GPT-5.4 returning valid JSON 95 times and weird prose on the 96th
  • Claude Opus 4.6 extracting the right answer but missing your exact schema
  • Grok 4.20 being good enough for one classification step but not another
  • retries for rate limits, but not retries for bad outputs
  • fallback prompts only after validation fails
  • parsing rules shared across multiple automations
  • provider swaps without rewriting the whole flow

You can model all of that with nodes and branches.

We did.

That's also how you end up with a workflow nobody wants to touch.

The hidden tax is confidence

The biggest problem wasn't breakage.

It was confidence.

Once your business logic lives across dozens of workflow nodes, every change feels dangerous.

Rename one field? Maybe three branches break.

Swap one model? Maybe parsing changes in five places.

Add one retry path? Maybe a fallback branch now loops in a way nobody expected.

This is where Make routers and Zapier Paths hit the same wall too. They look manageable at first. Then AI edge cases pile up and the whole thing turns into a visual choose-your-own-adventure with no real test suite.

What we tried before moving logic into code

We tried to be disciplined inside the workflow builder.

We:

  • cleaned up node names
  • added comments
  • split branches more clearly
  • pushed parsing into an n8n Code node
  • tightened prompts
  • added validation steps
  • added retries

That helped for about five minutes.

The real problem was architectural.

We were using orchestration software as an app runtime.

That meant:

  • versioning was bad
  • reviewing changes was bad
  • reusing logic was bad
  • testing was mostly "run it and see"
  • rollback was clumsy
  • provider comparisons were annoying
  • debugging meant clicking through execution trails instead of reading logs

The breaking point came when we wanted to swap providers without rewriting the automation.

That should be a config change.

Inside a node maze, it turns into surgery.

What should stay in n8n vs what should move to code

Here's the split I wish we'd used earlier.

Keep in n8n / Make / Zapier Move to code
Triggers Prompt construction
Webhook entry points JSON schema validation
Schedules Retry policy
Human approvals Fallback logic
App-to-app handoffs Provider abstraction
Slack / email / CRM actions Shared parsing rules
Database writes Testable business logic

Short version:

  • workflow tools should orchestrate
  • code should decide

What script-first actually looks like

You don't need to throw away n8n.

Just stop asking it to own the hardest part.

A much better pattern is:

  1. n8n receives the trigger
  2. n8n sends data to a script or internal service
  3. the service handles prompts, validation, retries, and model routing
  4. the service returns a stable payload
  5. n8n handles downstream actions

That service can be tiny. It doesn't need to be a giant platform.

It just needs to put the AI logic somewhere that Git, tests, logs, and refactors actually work.

A practical example

Let's say you have an n8n workflow that triages inbound support tickets.

The fragile version

Inside n8n:

  • call GPT-5.4
  • parse JSON
  • branch on category
  • retry on failure
  • call Claude Opus 4.6 if parsing fails
  • reformat output
  • branch again on urgency
  • send to Slack or Zendesk

This works until it doesn't.

The better version

n8n just calls your service:

curl -X POST http://internal-ai-service/triage \
  -H "Content-Type: application/json" \
  -d '{
    "ticket_id": "123",
    "subject": "Customer cannot log in",
    "body": "I reset my password twice and still get an error"
  }'
Enter fullscreen mode Exit fullscreen mode

Your service handles the messy part.

import OpenAI from "openai";
import { z } from "zod";

const TicketSchema = z.object({
  category: z.enum(["billing", "bug", "account", "other"]),
  urgency: z.enum(["low", "medium", "high"]),
  summary: z.string(),
  needs_human: z.boolean()
});

const client = new OpenAI({
  apiKey: process.env.STANDARD_COMPUTE_API_KEY,
  baseURL: "https://api.standardcompute.com/v1"
});

export async function triageTicket(input: { subject: string; body: string }) {
  const response = await client.chat.completions.create({
    model: "openai/gpt-5.4",
    messages: [
      {
        role: "system",
        content: "Classify support tickets and return strict JSON only."
      },
      {
        role: "user",
        content: `Subject: ${input.subject}\n\nBody: ${input.body}`
      }
    ],
    response_format: { type: "json_object" }
  });

  const raw = response.choices[0]?.message?.content ?? "{}";
  const parsed = TicketSchema.parse(JSON.parse(raw));

  return parsed;
}
Enter fullscreen mode Exit fullscreen mode

Now your workflow gets one stable response:

{
  "category": "account",
  "urgency": "high",
  "summary": "User cannot log in after password reset attempts.",
  "needs_human": true
}
Enter fullscreen mode Exit fullscreen mode

That is way easier to reason about than 8 branches and 3 parser nodes.

Why this is better for developers

Once the AI layer is code-first, you get normal engineering tools back.

1. Real version control

You can review prompt changes in Git.

git diff
Enter fullscreen mode Exit fullscreen mode

You can see exactly what changed in validation logic, retry policy, or provider selection.

2. Tests

You can write tests for the parts that matter.

import { describe, it, expect } from "vitest";

describe("triageTicket", () => {
  it("classifies login issues as account", async () => {
    const result = await triageTicket({
      subject: "Locked out",
      body: "Reset password twice, still can't log in"
    });

    expect(result.category).toBe("account");
  });
});
Enter fullscreen mode Exit fullscreen mode

No, LLM tests won't be perfectly deterministic.

They're still better than clicking "Execute Workflow" and hoping.

3. Shared logic

One parser. One schema. One retry policy.

Used everywhere.

Not copied across five automations.

4. Provider abstraction

If GPT-5.4 is best for one task but Claude Opus 4.6 is better for another, that should be hidden behind one internal interface.

Something like:

export async function classify(input: string, provider: "gpt" | "claude" | "grok") {
  const modelMap = {
    gpt: "openai/gpt-5.4",
    claude: "anthropic/claude-opus-4.6",
    grok: "xai/grok-4.20"
  };

  return client.chat.completions.create({
    model: modelMap[provider],
    messages: [{ role: "user", content: input }]
  });
}
Enter fullscreen mode Exit fullscreen mode

That is a small change in code.

Inside a giant workflow, it often becomes a full rewrite.

Why this matters even more for 24/7 agents

If a workflow runs twice a day, you can tolerate some mess.

If an agent runs all day processing tickets, leads, documents, or support threads, every weak spot turns into operational drag.

A flaky branch isn't a minor annoyance anymore.

It's a permanent tax.

This is also where cost and throughput start to matter.

Teams running AI agents in n8n, Make, Zapier, OpenClaw, or custom workflows usually do not want to babysit token spend while background jobs keep firing.

They want the automation to run.

That is one reason Standard Compute is interesting in this setup.

It gives you an OpenAI-compatible endpoint, so you can keep using the OpenAI SDK or any compatible HTTP client, while routing across GPT-5.4, Claude Opus 4.6, and Grok 4.20 behind the scenes.

More importantly, it's flat-rate instead of per-token.

That matters a lot for always-on automations, because architecture decisions stop being distorted by token anxiety.

You can keep the script-first pattern, keep your orchestration layer the same, and avoid rebuilding workflows every time model pricing or provider quality shifts.

A simple architecture that works

If I were starting over, I'd use this split:

n8n / Make / Zapier
  -> trigger, schedule, webhook, notifications
  -> call internal AI service

Internal AI service
  -> prompt construction
  -> model routing
  -> schema validation
  -> retries
  -> logging
  -> tests

Downstream systems
  -> Slack
  -> CRM
  -> database
  -> email
Enter fullscreen mode Exit fullscreen mode

Boring architecture is underrated.

This one is boring in the best possible way.

Actionable rule of thumb

If your workflow has any of these, move the AI logic into code:

  • more than one model provider
  • strict JSON requirements
  • retries based on failure type
  • shared prompt logic
  • reusable validation
  • complex branching
  • production consequences when it breaks

Keep the workflow builder for orchestration.

Keep the brain in code.

Final take

The issue was never that n8n was bad.

The issue was that we kept asking n8n to do a job better handled by code.

Use n8n for orchestration.
Use Make for orchestration.
Use Zapier for orchestration.

But if your AI workflow has real logic, real retries, real validation, and real provider switching, put that part in a script or service.

We learned that after rebuilding broken flows more times than I'd like to admit.

I wouldn't go back.

If you're already running AI automations this way, I'm curious where you draw the line between workflow builder and code.

Top comments (0)