A client asked me in July to stop forwarding her contact form leads to a spreadsheet and just handle them. Score the lead, draft a reply, ping her on Slack if it looked like a real budget, archive it if it looked like a recruiter spamming her about a job she didn't want. I built the whole thing in n8n in an afternoon. It ran clean for six weeks. Then it silently stopped tagging anything as high-priority, and nobody noticed until she asked why she hadn't heard from a promising lead in eleven days.
The workflow, roughly
Here's the shape of it, trimmed to the part that matters. A webhook catches the form submission, a Function node scores it, an If node branches on that score:
{
"nodes": [
{ "name": "Webhook", "type": "n8n-nodes-base.webhook" },
{ "name": "Score Lead", "type": "n8n-nodes-base.function" },
{ "name": "High Priority?", "type": "n8n-nodes-base.if" },
{ "name": "Slack Alert", "type": "n8n-nodes-base.slack" },
{ "name": "Draft Reply", "type": "n8n-nodes-base.httpRequest" }
]
}
And the actual scoring logic, which is where the whole thing quietly broke:
const budget = $input.item.json.body.budget || "";
const message = $input.item.json.body.message || "";
const score = budget.includes("$") ? 3 : message.length > 200 ? 1 : 0;
return { json: { ...$input.item.json.body, score } };
That body.budget field existed because the form on her site sent it as a top-level key. In August she redesigned the form with a page builder plugin, and the plugin nested every field under a formData object instead of putting it at the root. The webhook still fired. The payload still arrived. $input.item.json.body.budget just quietly evaluated to undefined, "".includes("$") returned false every single time, and every lead scored zero regardless of budget. No error, no failed execution in the n8n logs, because nothing actually threw.
Why this is worse than a normal outage
A crashed workflow is loud. n8n's execution list turns red, and if you've wired up error workflows (you should, and I hadn't on this one) you get a notification within seconds. A workflow that runs to completion and produces a wrong answer is silent by design, because as far as n8n is concerned, nothing went wrong. The Function node executed, returned valid JSON, and passed it along. The bug lived entirely in an assumption about the shape of somebody else's data, which is exactly the kind of thing that survives every test you write yourself because you're the one who wrote the test data too.
I now add a one-line sanity check at the top of every scoring function that touches an external payload:
if (!("budget" in ($input.item.json.body || {}))) {
throw new Error("payload shape changed: no budget field");
}
Cheap, ugly, and it turns a silent miscategorization into a loud failed execution that shows up in n8n's error workflow. I'd rather get paged for a broken assumption than find out from a client eleven days later.
How I actually caught it
She noticed the drought before I did, which tells you everything about how I was monitoring this thing at the time: not at all. I'd built the workflow, watched it work for a week, and moved on to the next client project, the classic freelancer mistake of treating a shipped automation like a shipped feature instead of a running service. Once she flagged it, finding the bug took ten minutes with n8n's execution history, since every run was sitting right there with its input and output JSON. Finding out it had been broken for six weeks took a client noticing a gap in her own pipeline, and that part I actually feel bad about.
What I run now on every client workflow that touches money or leads: a second, tiny n8n workflow on a daily cron that queries the main workflow's execution count for the last 24 hours through n8n's own API, and pings me on Slack if that count drops to zero or if the average score across all of yesterday's leads is suspiciously flat. A flat score distribution was exactly the fingerprint of my bug: every lead scoring 0, day after day. A simple standard-deviation check on that score would have caught it on day one instead of day forty-two. It's maybe fifteen lines of Function-node code, and it's a lot cheaper than the one honest conversation I had to have about why a warm lead went cold for over a month.
n8n's pace is part of the story here
n8n ships fast enough that "which version am I even running" is a real question worth asking before you debug anything else. Checking the n8n releases page while writing this, the project cut nine separate tagged releases across its stable and beta lines in the four days leading up to September 25 alone. If you're self-hosting via Docker and you pull n8nio/n8n:latest on a cron job the way a lot of tutorials tell you to, you can wake up to genuinely different node behavior with zero warning. I pin a specific tag now and read the changelog before bumping it, the same discipline I'd apply to any other dependency, which n8n absolutely is even though the interface makes it feel like a website you're clicking around in rather than software you're running.
Where n8n actually earns its keep over Zapier or Make
For a client on a budget, n8n's self-hosted option removes the per-task pricing that makes Zapier expensive the moment a workflow runs more than a few hundred times a month. I've moved three client automations off Zapier this year purely on cost, not features. The tradeoff is that you now own the box it runs on, the Postgres database behind it, and the debugging when a node behaves differently than the docs suggest. That's a fair trade for a workflow that runs thousands of times a month; it's a bad trade for something a client runs twice a week, where Zapier's hosted reliability is worth the per-task fee and the lack of a server to patch.
I wrote about the harder end of this same tradeoff, an automation that actively hurt a client relationship instead of just quietly under-delivering, in the refund bot that cost me a client. Silent miscategorization and an overconfident refund bot are the same root problem wearing different clothes: automation that never tells you when its assumptions stopped holding.
What to check this week if you run anything on n8n
Open your busiest workflow and look for any Function node that reaches into a nested field from a webhook, form, or third-party API payload. Add the one-line guard above to each one. Then go check whether you've actually wired an error workflow to catch failed executions, not just left the default "do nothing" behavior, because that's the notification that would have caught my lead-scoring bug in week one instead of week six. If you're doing freelance automation work for clients, this is also the kind of detail worth mentioning up front; I cover how I scope that conversation in my writeup on pricing this work honestly, and you can see the rest of what I build at abrarqasim.com.
Originally published at abrarqasim.com. I write there about React, PHP, Rust, Go and the AI tooling around them.
Top comments (0)