Sharing this because it bit me and I do not see it talked about much.
If you run an AI Agent node with tools, the agent can finish “green” even when one of its tools failed. When a tool errors, the error often does not get passed back to the agent, the agent node itself does not fail, so the execution is never marked failed. The agent just fills the gap with a plausible guess and carries on. There is an open issue on the n8n GitHub about it, and it is on the AI Agent common-issues docs page too, so it is not just me.
For a normal flow a silent failure is a missing row. For an agent it is a confident wrong answer handed downstream while every log stays green. If that flow is client-facing, you shipped a made-up answer and nothing alerted you.
What helped me:
Tighten tool output schemas so a bad or empty response throws instead of returning quietly
Wire an error output off the agent, or an error workflow, so a tool failure actually goes somewhere
A quick sanity check on the agent output before anything downstream uses it
Those shrink the problem but do not fully fix it, since they live inside the same run that reports itself healthy.
The pattern I trust is a receipt before the downstream step runs.
Not just “tool failed”, but tool called, input shape, output source, empty-result state, and whether the next write was allowed. If that receipt is missing, the flow should stop even if the agent node is green.
The receipt pattern Ahmad mentioned is solid. One extra layer I add on the agent output: a Code node that scans $json.output for known failure signals (“I couldn’t”, “failed to”, “I was unable to”, “I don’t have access”) and throws an error if any match. It’s blunt but catches the hallucination-covering-tool-failure case before it reaches anything downstream - especially useful when the tool failure message leaks into the agent’s response text.
yeah, receipt is the right framing. “tool failed” is binary and misses most of it. what you actually want is provenance: was the tool called, did the output come from it or did the model fill it in, was the result empty, was the write allowed. no receipt, no downstream. that beats green vs red by a mile.
the hard part for me is keeping it consistent. you have to define a valid receipt per tool and wire the check on every downstream step, and the flow that bites is always the one where someone skipped it under deadline. so I am sold on it as a standard, I just do not trust every flow to actually carry one. how are you generating the receipt, do you build it into each agent by hand or do you have a reusable pattern for it?
nice, saving that one. it nails the case where the tool error leaks into the text. the one it misses is the confident fabrication, where the agent makes up a clean plausible answer with none of those phrases and it just sails through. that’s the one that still gets me. does legit output ever trip a false positive for you?
Rare false positives in my experience - the phrases I use are hedging/apology patterns that don’t appear in clean tool output unless your tool itself returns error-style text. If your tool returns things like “I was unable to find matching records” as a legitimate empty result, that would trip it - so worth tuning the phrase list to your specific tools.
The confident fabrication case you describe is harder. The only real defense there is structural: validate the output against expected schema or check that specific fields exist and aren’t empty. If your tool should return a record ID and the agent output has none, that’s a detectable signal regardless of how confident the text sounds. An IF node checking $json.output for required fields is more reliable than text matching for that scenario.
I would avoid making every agent build it by hand. I’d put a thin wrapper around each risky tool: normalize input, call the tool, write run id, input digest, output source, empty or error state, and downstream permission.
Then downstream nodes trust the receipt, not the model text. The per-tool part is just the schema.
The thin wrapper is the right call. Building it into every agent just means you reimplement the same five fields with subtle differences each time, and the differences are where the gaps hide. The field that earns its keep is the empty-or-error state, because the failure we actually care about is the tool returning a clean empty payload that the model then narrates as a success. The wrapper catches “the tool gave nothing” even when nothing threw.
One thing I would add: make the wrapper output replace the raw tool output, not sit next to it. If a downstream node can still reach the raw text, eventually one will, and the receipt stops being the source of truth. Do you version the schema per tool, or keep one shared shape?
For most workflows I keep one shared shape with a tool_id field stamped at call time - it keeps schema maintenance low and the downstream IF node only has one contract to check. Per-tool versioning only makes sense when different tools have meaningfully different failure modes you need to route on separately. When I hit that point I usually move each tool into its own sub-workflow and let the wrapper live there, so the main agent stays clean.
One shared shape with a tool_id stamped at call time is where I landed too,
for the same reason you gave: the downstream IF node stays a single contract
to check. The place it bit me was a new tool getting added without the branch
knowing about it, so the receipt was there but the else path read it as a
pass. Now an unknown tool_id routes to failure by default, so anything the
schema has not seen is treated as untrusted instead of silently fine.
The sub-workflow move you mentioned is the part I underrate. Once the wrapper
lives next to the tool, the main agent stops carrying five fields it never
reads, and the empty-or-error logic is testable on its own. I only split when
a tool genuinely needs different routing though, otherwise it turns into a
pile of sub-workflows to keep honest.
The receipt approach is exactly right for catching what happens inside the agent execution. There is a complementary gap on the outside.
For workflows that handle inbound business events – booking requests, form submissions, lead notifications – the trigger can stop firing entirely while the workflow stays perfectly quiet. No executions, nothing to receipt-check, everything green. A webhook endpoint that silently changed, or an OAuth refresh that died on the inbound trigger, produces the same “healthy” result: zero recent executions, no errors to catch, no provenance chain to inspect because the chain never started.
The fix for that layer is monitoring trigger rate from outside the workflow: if you normally see 10-15 trigger events per day from a given source and the count drops to zero for 12 hours, that is worth an alert regardless of what the AI Agent node is doing. A synthetic test event – a daily test webhook ping or a test form submission – that must complete end-to-end catches dead connections before real traffic misses them.
The receipt/provenance pattern and the trigger-rate monitor are complementary. Receipt handles execution correctness (what you’ve described here); rate monitor handles trigger presence. For any workflow where missing an inbound event has a cost, you need both shells.
The trigger-rate plus synthetic canary is the right outside shell, and the two together do more than cover two failures, they disambiguate. A rate drop to zero on its own has the same false-alarm problem every absence check has, a dead webhook and a genuinely quiet Sunday both read as zero. Cross it with the canary and it resolves: the canary still completing while real events sit at zero is a real quiet period, the canary failing too means the path is actually dead. So the pair is not just receipt-for-correctness plus rate-for-presence, it also tells you which of the two you are looking at before you page someone at 3am. Per-source baselines matter for the same reason, a global rate stays healthy while one source quietly dies.