Everything above detects deviation from a baseline. There is a class underneath that: the workflow that never fired even once. Two of these bit us, and both are invisible to every layer in this thread.
1. A schedule trigger that is Active and never fires.
If a Schedule Trigger set to the weeks interval is missing weeksInterval in the workflow JSON, it never fires — not late, not once. We confirmed this two ways on n8n 2.31.5: reading the recurrence check in the source, and publishing a copy of the file exactly as shipped and watching nothing happen. A missing triggerAtMinute is the same family — it quietly becomes a hash-derived pseudo-random minute instead of the one you meant.
Two things make this hard to catch before it ships:
- Manual execution skips the recurrence check entirely. “I tested it and it ran fine” carries no information about whether the trigger will ever fire on its own.
- Opening and saving the trigger node once in the UI normalizes the JSON and fills the missing field in. A workflow that is broken as a file becomes correct the moment you inspect it in the editor, so UI-based testing cannot prove the file you shipped or imported.
Why the layers above miss it: layer 1 needs an execution to fail, and layer 3 and the external dead-man’s switch both need a “usual interval” or a first ping to compare against. “Has not run in longer than usual” has no usual when the true answer is never. Zero executions since publish deserves to be its own alarm, separate from stopped running.
The check we run now: publish the workflow exactly as it exists as a file, without opening the trigger node, then wait for a production execution. In the Executions list, scheduled runs have no flask icon and manual runs do — that is the machine-checkable proof that the schedule fired rather than you.
Adjacent, same silence: the hour in a Schedule Trigger is interpreted in the instance’s timezone (Workflow Settings → Timezone), not yours. Setting 15:38 on a US-Central machine whose instance defaulted to America/New_York meant 14:38 local time, already in the past, so that day’s run simply did not happen.
2. A disabled node passes every validation layer and quietly shortens the output.
A node left "disabled": true is excluded from the activation check — we read this in three places in our own 2.31.5 install: the server-side validation service, the activation path that calls it, and the frontend bundle. On production runs a disabled node also passes its input straight through. So the execution reports success, the output is wrong in exactly the “runs fine, output is wrong” way described upthread, and nothing anywhere flags it. This one is introduced at edit time rather than by an upstream change, so a canary catches it only if the canary runs through the same path.
Scope caveat: all of the above is measured on self-hosted 2.31.5 and 2.32.6. We have no Cloud measurements of our own.