n8n vs Custom Code for Engineering Automation: The Decision, and the Bug That Proved It Right
Why I built my own distribution pipeline in versioned code instead of n8n, and the silent-fail bug that came out of it, one a GUI workflow tool would have hidden.
This site runs on a small pipeline: content queue, approval, platform adapters, retry logic. Before writing a line of it, I had to decide how to build it. n8n was the obvious default, drag a few nodes, wire up the APIs, done in an afternoon. I didn’t use it. I wrote the pipeline as plain code in this repo instead.
That call turned out to matter more than I expected, and not for the reason I originally gave.
The decision, as I made it
Four reasons, in the order I actually weighed them:
- The pipeline is versioned and reviewed like any other code. Every change to how a post gets approved or published goes through the same PR review the rest of the site does. A workflow built in a GUI tool doesn’t get that for free, you’d need to bolt on export/diff tooling to get anything comparable.
- I run it headless. Claude Code operates the queue directly, no dashboard, no clicking through a canvas to see what a node does. A GUI automation tool is built around a human opening a browser tab to check on it.
- I already had the approval infrastructure. A Telegram bot, wired to the queue, no new service to stand up.
- n8n is one more thing to run and patch. A VPS process with its own uptime, its own security surface, its own upgrade cadence, for a job three adapters and a cron loop already do.
None of those four are exotic. Any team choosing between a workflow tool and custom code for internal automation runs roughly this same checklist. What made the decision worth writing about is what happened five weeks later.
The bug the code caught
On 2026-07-20, one leg of a scheduled post failed. Buffer’s API returned an error, but the adapter I’d written treated the response as a plain status string instead of checking it. publish.py read that string, didn’t see anything it recognized as a failure, and archived the post as “Published.” The X leg never went out, and it dropped out of the retry queue because the pipeline believed it was done.
I caught it because the code is code. I read buffer.py, saw exactly where the status check was missing, and fixed the one line that mattered: raise on status == "error" instead of returning it as text. The fix itself went through the pipeline’s own guardrail, a headless run opens a PR and stops, no auto-merge, so even the fix to the failure-handling didn’t silently ship itself.
Here’s the part I actually want another engineering lead to sit with: I don’t know what the equivalent debugging session looks like in n8n. Not “it would have been harder”, I mean I genuinely don’t have a mental model for it, because a workflow canvas doesn’t give you the same thing a stack trace and a git blame give you. You’d be reading node output logs and guessing at what a black-box HTTP node considered “success.” The bug wasn’t really about Buffer’s API. It was about whether a false positive is legible to the person debugging it, six months after they wired the thing up and forgot the details.
What this actually is
I didn’t avoid n8n because visual workflow tools are bad. They’re good at what they’re built for, non-engineers assembling integrations without writing code, fast prototyping across a lot of SaaS APIs, a team that doesn’t want another service in its own stack to own. None of that was my situation. I’m the one engineer running this, I already write the code that runs everything else on the site, and the actual risk I needed to manage wasn’t “how fast can I wire this up”, it was “will I be able to tell when this silently does the wrong thing.”
That’s the actual decision framework, not “code beats no-code.” Ask what happens when the automation is wrong in a way that doesn’t throw an error. If the answer is “I’d have no idea,” the tool matters less than whether you can read what it did.
distribution-plan.md in this repo still has the door open for n8n later, gated behind the current stage running a week without manual patching. It hasn’t needed it. The bug that would have made the strongest case for a GUI’s visibility turned out to be the same bug that proved the opposite: a workflow you can read line by line is a workflow you can actually debug.
What’s the automation decision you made for reasons that turned out to be right for a completely different reason than you expected?