
Why Your n8n Error Workflow Never Fires
An n8n error workflow runs on execution failure — not on “something went wrong.” Those are different events, and a tool that blows up inside an AI Agent usually only qualifies as the second one. In the older tool path the error is caught and handed back to the model as a plain string. In the current one it's thrown, and then quietly converted into a data field if the Agent node's On Error is anything but Stop Workflow. Both roads end the same way: a green execution, a wrong answer, and an Error Trigger that never ran.
What Actually Starts an Error Workflow
The n8n docs are precise about this and most people skim it. You set an error workflow in Workflow Settings, and it runs if an execution fails. That's the whole contract. Not “if a node turns red.” Not “if an API returned 500.” The execution status has to land on error.
There's a second gotcha in the same docs that eats an afternoon if you don't know it: you can't test an error workflow by running things manually.The Error Trigger only fires for automatic executions. I've watched people conclude their alerting was broken when it was working fine — they were just clicking Execute Workflow and expecting a Slack message.
So when someone tells me their error workflow “stopped working,“ the first question isn't about the error workflow. It's: did the run actually fail? Open the executions list and look at the status column. Nine times out of ten it says Success, and the real bug is upstream.
Why a Failed Tool Doesn't Fail the Run
Here's the part that surprised me when I finally went and read the source instead of guessing. n8n has two different code paths for calling a tool from an AI Agent, and they handle failure in opposite ways.
The first is the dynamic-tool wrapper in N8nTool.ts, used for the fallback path and for community tools built on it. Its error handling is three lines, and they explain a whole class of bug:
try {
const result = await func(parsedQuery);
return result;
} catch (e) {
context.addOutputData(NodeConnectionTypes.AiTool, index, e);
return e.toString(); // <- the error becomes the tool's answer
}Read that last line again. The exception is stringified and returned as the tool's result. The model asked “what's this customer's balance,” the Postgres node threw, and what came back into the model's context was the text of a connection error. Nothing threw. The run is green.
The addOutputData call on the line above is why this is so disorienting — it paints the tool sub-node red in the execution log. You open the run, you see a red node, and you reasonably assume the execution failed. It didn't. A red sub-node in a successful execution is exactly the signature of a swallowed tool error.
The Second Path Throws — Then Something Catches It
The modern path, used when you attach a regular n8n node as a tool, behaves better. It lives in makeHandleToolInvocation, it retries according to the tool node's own settings, and on the final attempt it throws — the source comment literally says “If this is the last attempt, throw the error to properly terminate execution.”
So the agent node does fail. And then this happens, one level up, where the agent collects its batch results:
if (ctx.continueOnFail()) {
returnData.push({
json: { error: error.message },
pairedItem: { item: itemIndex },
});
return; // execution stays SUCCESSFUL
} else {
throw new NodeOperationError(ctx.getNode(), error);
}One boolean decides whether your on-call phone rings. And continueOnFail() is just the Agent node's On Error setting being anything other than Stop Workflow. If it is, the error stops being an error and becomes a json.error field on an item, riding along in a perfectly successful execution.
This is where people get burned by a setting they turned on for good reasons. Continue (using error output)sounds like the responsible, grown-up choice — you get a dedicated branch to handle problems. But it also silences the global net. You've opted out of platform-level alerting in exchange for a branch you now have to handle perfectly, every time, in every workflow.
Which Failures Reach the Error Trigger?
| What went wrong | Execution status | Error workflow? |
|---|---|---|
| Regular node throws, On Error = Stop Workflow | Error | Yes |
| Regular node throws, On Error = Continue | Success | No |
| Node-as-tool throws, Agent On Error = Stop Workflow | Error | Yes |
| Node-as-tool throws, Agent On Error = Continue (either kind) | Success | No |
Dynamic-tool path catches the error (N8nTool) | Success | No |
| Execution cancelled mid-tool-call | Cancelled | No |
| Stop And Error node | Error | Yes |
That cancellation row deserves a note, because it's the strangest thing in the file. When you cancel a run while a tool is mid-flight, n8n doesn't throw — it returns the literal string "Error during node execution: Execution was cancelled" to the model. Your agent gets one last chance to read that sentence and improvise a response with it.
The Fix: Assert, Then Fail Loudly
You can't patch the tool wrapper, so stop trying to make the agent fail correctly and instead check its work. Four changes, in the order I apply them:
- Put the Agent node's On Error back to Stop Workflow.This is one dropdown and it restores the entire path from a thrown tool error to your Error Trigger. If you switched it to Continue while debugging six months ago, it's still there.
- Assert on the artifact, never the prose. After the agent, an IF or Code node that checks the thing that was supposed to happen actually happened — a row ID came back, a count is above zero, a status field says
ok. An agent that reads a connection error out of a tool result will happily tell you it “updated the record.” - Send failed assertions into Stop And Error.This is the one node that manufactures a real execution failure on demand, with your own message. It's the bridge between “I detected a problem” and “the platform knows there was a problem.”
- Enable Retry On Fail on the tool node, not the agent.n8n clamps tool retries to 2–5 attempts (default 3) and 0–5000ms between them (default 1000). Each attempt gets its own run index, so a flapping API shows up as repeated entries in the log rather than one mystery slowdown.
This is the same lesson I wrote up after rebuilding three production agents: deterministic control flow beats clever prompting. An assertion node is boring and it is not negotiable. If you also want to see why the agent chose a broken tool in the first place, that's a tracing problem rather than an error-handling one.
A Cheap Canary Worth Building Once
Even with all four fixes, error workflows only tell you about runs that started and failed. They say nothing about a workflow that stopped being triggered at all — a webhook that went quiet, a schedule that got deactivated during a deploy. Silence looks identical to health.
So I run one extra workflow per environment: a schedule that queries the n8n API for executions in the last N minutes and calls Stop And Error when a critical workflow's count is zero. It's maybe fifteen minutes of work and it's caught more real incidents for me than the error workflow has, because the failures that actually hurt tend to be the ones that produce no execution at all.
Two adjacent things worth reading if you're hardening a production instance: how to decide whether a capability should be a tool or a sub-workflow (sub-workflows give you an error boundary a tool doesn't), and how to stop retries from double-writing your data. Turning on Retry On Fail without idempotency just converts one failure into three duplicate records.
Key Takeaways
- Error workflows are bound to execution failure, not to things going wrong. If the executions list says Success, the error workflow was never supposed to run.
- The dynamic-tool path in N8nTool.ts returns e.toString() as the tool's result — the model reads your stack trace as data and answers anyway.
- A red tool sub-node inside a green execution is the signature of a swallowed tool error, not a failed run.
- Any On Error value other than Stop Workflow makes continueOnFail() true, which turns a thrown tool error into a json.error field on a successful execution.
- Continue (using error output) does not trigger the error workflow. You traded global alerting for a branch you now have to handle everywhere.
- Assert on the artifact after the agent, and route failures into Stop And Error — that's the only way to manufacture a real failure the platform can see.
- You cannot test an error workflow with a manual execution. The Error Trigger only fires on automatic runs.
Not Sure What Your Agents Are Silently Failing At?
I build and audit production n8n and AI agent systems — including the unglamorous layer that decides whether you find out about a failure in ten seconds or ten days. If your dashboard is all green and you don't fully trust it, that's the job.
Related Posts
n8n
n8n Cloud vs Self-Hosted in 2026: The Real Math
n8n removed active-workflow limits from every plan in 2026, so the old reason to self-host is gone. What you buy now is a monthly execution allowance and a concurrency ceiling — and the jump from Pro to Business is 4x the executions for 13x the price. Measured numbers from my own instance: 179 workflows, ~5,600 executions a month, a median run under one second, and why AI agent workflows break the concurrency assumption entirely.
n8n
How to Stop Prompt Injection in n8n AI Agents
Guard prompts don't stop indirect prompt injection — they're just one more instruction competing on the same channel. The four layers that do work, plus what a tool-scoping pass over 182 production workflows actually found.
n8n
n8n 3.0 Breaking Changes: What Actually Breaks
n8n 3.0 drops npm installs, AI Agent node v1, and three legacy nodes. I audited 182 production workflows against the full removal list — 12 nodes flagged, 11 needed a version bump, exactly 1 needed a rewrite.