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:
- n8n receives the trigger
- n8n sends data to a script or internal service
- the service handles prompts, validation, retries, and model routing
- the service returns a stable payload
- 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"
}'
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;
}
Now your workflow gets one stable response:
{
"category": "account",
"urgency": "high",
"summary": "User cannot log in after password reset attempts.",
"needs_human": true
}
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
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");
});
});
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 }]
});
}
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
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)