How do you catch workflows that run fine but do nothing?

How do you catch workflows that run fine but do nothing?

I’ve been running client automations in n8n for a while and keep hitting the same failure, and I’m curious how everyone else deals with it.
The workflow executes. Every node is green. No errors thrown. And zero records actually landed.
Usually it’s something quiet upstream: a token expired, a field got renamed, an API changed its response shape, or a filter suddenly matches nothing. The error workflow never fires, because from the system’s point of view nothing went wrong.
The part that costs me isn’t the fix, it’s the delay. By the time anyone notices, it’s been broken for days.
So, especially for those of you running workflows for clients:
• Do you check anything beyond the error workflow?
• Has anyone wired up something external for this (Healthchecks.io, cron pings, a custom heartbeat node)?
• Do you validate record counts, or just that the run completed?
• Or, honestly, do you usually find out when the client tells you?
That last one is the one I’m trying to stop happening. Interested to hear what’s actually working for people.

Hey @rodri.galloswork, while you wait for a response, here are some things that might help:

Suggested resources

Automatically matched to your question.

Docs:

Forum:

@ShawnWilliams, @Dan_Westness, @Niffzy - you’ve helped with similar issues before, can you take a look?

Automatically suggested by n8n’s community bot. It’s a pilot - please share feedback here.

Yes—the useful distinction is execution health versus outcome health. I normally split it into three checks:

  1. Run health: start/end, duration, retries, upstream status, and the execution error path.
  2. Output health: expected count window, freshness, required fields, schema hash, and duplicate-key checks.
  3. Receipt health: a downstream record ID, acknowledgement, or an explicit exception entry.

A green run with an unexpected zero should be review, not success. Keep the raw run evidence, and alert on consecutive misses so one legitimate empty result does not create noise.

I put the boundary into a small synthetic lab here: n8n Silent Success Lab | Romeo Apps

It is RomeoApps-owned fixture data, not client telemetry, and uses no credentials. For a real workflow, a redacted execution, expected output window, and destination acknowledgement are usually enough to identify the first missing check.

Good breakdown above on execution vs output vs receipt health. On the practical n8n side, here’s a lightweight pattern that catches this without extra infra:

  • An Error Trigger workflow alone won’t help here — it only fires on unhandled exceptions, and your problem is exactly the case where nothing throws.
  • End your main workflow with an IF node checking the actual item count (or a specific field) coming out of the last “real” node. If it’s 0 or doesn’t match what you expect, route it into a Stop and Error node — that forces n8n to treat an empty/wrong result as a failure, which then does trigger your error workflow / notification.
  • For the “silent for days” problem specifically, a separate Schedule Trigger heartbeat workflow works well: every N minutes/hours it checks (HTTP Request or a DB query) whether the expected downstream record was created recently, and alerts you if not.
  • Put the record-count check as close as possible to the actual write node (right after the Kintone/DB/API write), not at the very end of the workflow — that way you catch it before other branches keep running on bad data.

The pattern that’s saved me the most: never let “workflow finished without error” be your only success signal — always add one explicit assertion node before the workflow is allowed to end green.

Hi @rodri.galloswork

This is a well-known and important problem. As others have said, you need to tell the difference between a workflow that errors and a workflow that runs successfully but produces nothing. You can’t just rely on the built-in error workflow for the second case.

You can add an assertion check at the end of your flow, but I prefer a “gatekeeper” check right at the beginning. It’s more efficient because it stops the rest of your workflow from running if there’s no data to process.

The pattern is simple:

  • After your main query (e.g., “Get Records from DB”, “HTTP Request”), add an IF node.

  • Set the IF node to check if the number of incoming items is equal to 0.

  • The true path is the one that leads to “failure”. This is where you can set what to do when certain things happen (e.g., send a Slack message, create a Jira ticket, send an email). You can add a Stop and Error node here to trigger your global error workflow.

  • The false branch is your “success” path, which continues to the rest of your normal workflow logic.

This way, the workflow only proceeds if it has data, and you get an immediate alert if it pulls nothing.

Hope this help.

this is solid, especially the part about putting the count check right after the write node and not at the end. thats a mistake i see a lot where people check too late and bad data already propagated downstream

the separate heartbeat workflow approach is basically what i landed on too when building around this. the part that gets tricky at scale tho is maintaining one of those per client workflow, when you get to 15 or 20 automations across different clients the manual heartbeat setup starts eating real time. thats been the main driver for me to build something that handles it outside the workflow itself

the gatekeeper at the start is smart, hadnt thought about it that way. stops the whole chain from running on empty data instead of letting it go through and checking at the end. definitely more efficient

one thing i keep running into with this pattern tho, it works great per workflow but doesnt solve the “workflow just stopped running entirely” case. like if the schedule trigger itself stops firing for whatever reason, the IF node never even gets a chance to check. thats why the heartbeat layer on top of it matters, something external that notices the workflow went quiet, not just that it ran empty

On the “one heartbeat per client workflow doesn’t scale” part — the thing that fixed this for me was inverting it: instead of every workflow owning its own monitor, have every workflow report in to one shared checkpoint, and run a single watcher over that table.

Concretely:

  1. One tiny sub-workflow, checkin, called via Execute Workflow at the end of each pipeline (and ideally right after the write node, as nathan3 said). It takes three values: pipeline (a stable name), count (items actually written), ok (boolean). It upserts one row into a heartbeats table: pipeline, last_seen, last_count, last_ok. Three fields, one node to add per workflow, no per-client monitoring workflow.

  2. One expectations table you fill in by hand once per pipeline: pipeline, max_silence_minutes, min_count. This is the part worth doing consciously — writing down “this should run at least hourly and land at least 1 record” is what turns monitoring from vibes into a check.

  3. One Schedule Trigger workflow, every 15 min, that joins the two: alert if now - last_seen > max_silence_minutes (catches the “trigger stopped firing entirely” case you mentioned, because absence of a row update is itself the signal), or last_count < min_count, or last_ok = false. Adding client #21 is one row in expectations plus one Execute Workflow node, not a new heartbeat workflow.

Two things that saved me pain with this:

  • Alert on N consecutive misses, not the first one, for anything with legitimately empty windows. Otherwise the weekend quiet period trains everyone to ignore the channel.
  • Make the watcher itself heartbeat somewhere external (a cron ping service is fine). Everything above lives inside n8n, so if the instance is down, the watcher is down too and you get silence instead of an alert. That’s one external dependency and it covers the whole fleet rather than one per workflow.

On “does the schedule still fire” specifically: if you’re self-hosted you can also read this straight from the n8n REST API — GET /api/v1/executions?workflowId=...&limit=1 and compare startedAt against the expected cadence. Same watcher workflow, no instrumentation in the monitored workflows at all, though you lose the “ran but wrote nothing” signal, so I’d use it alongside the check-in rows rather than instead of them.

Count == 0 catches the loud version of this. The one that’s bitten us harder is count > 0 but wrong: N rows land, the assertion passes, and every one of them has a null in the field that mattered because an upstream field got renamed and your write node still accepted the row. Nothing in this thread’s IF-node-on-count pattern catches that, because it’s checking cardinality, not shape.

The fix is the same shape of node, just asserting a different thing: after the write, check that the two or three fields you actually depend on are non-null and roughly the right type, not that the item count is nonzero. Costs one more condition on the same IF node nathan3 already described, and it catches the failure mode where a rename or an API contract change degrades the data instead of zeroing it out, which is the more common way a schema change actually breaks something in my experience.

One more thing worth building in if you bill or invoice off these runs at all: route that same assertion into whatever decides “did this run count,” not a second, looser check. Easy to end up in a state where the strict version alerts you but a separate, older gate is still what decides whether the client gets charged for the run, and those two silently drift apart over time.

themineworks’ point is the one I would underline: count is cardinality, not correctness, and the failure that actually costs you is N rows landing where the field you depend on is null, or worse, present and plausible but wrong. A field gets renamed upstream, your write node still accepts the row, and now you have real looking data that is quietly incorrect. Null and type assertions catch the version where it degrades to empty. They do not catch the version where it degrades to something plausible, non null, the right type, and simply wrong, because nothing about the row looks off.

The one check that catches that is a known answer canary. Alongside the real traffic, push a handful of synthetic inputs whose correct output you wrote down in advance, run them through the live pipeline end to end on a schedule, and assert the output equals the known answer, not just that it is shaped right. If a mapping silently starts pointing at the wrong but valid field, the canary’s expected value stops matching and you get a loud failure the same day, because you are comparing against truth rather than against plausibility.

It also closes the gap Cam raised. A canary that fails to report is itself the alert, so the same mechanism catches both the pipeline going quiet and the values going subtly wrong, with one external check watching for the canary result instead of one heartbeat per workflow. In practice I run three layers: count and null or type assertions right after the write for the loud failures, volume watermarks for the slow bleed, and a scheduled known answer canary for the plausible but wrong case the first two structurally cannot see.

The count > 0 but wrong-shape case is the one I’d worry about too.

A useful pattern is to make the final “success” check prove the artifact you care about exists, not just that something got written. For example: downstream ID exists, required business fields are non-null, source ID is preserved, and the run ID is attached somewhere you can audit later.

Otherwise you end up with a green execution and a destination full of records nobody can actually use.

@rodri.galloswork you mentioned maintaining per-workflow heartbeats starts eating real time at 15–20 automations across clients, and that it’s pushing you to build something outside the workflows. That’s something I’ve been looking at as well..(I’m a dev, learning before deciding whether to build).

The last time a client workflow went green-but-did-nothing, how long was it broken before anyone noticed, and who noticed first, you or the client?
What would “handles it outside the workflow” need to do for you to actually trust it?

Found something on 2.36.7 that I’d assumed worked differently.

When an IF condition stops matching, the node on that branch isn’t marked skipped. It’s not in runData at all. The execution still returns status: success.

So if you’re iterating nodes looking for a bad status, you find nothing. The signal is a key that isn’t there.

Repro: schedule trigger, code node emitting rows, IF that can never match, code node on the true branch. That last node is just missing.

Everything in this thread tells you when a run went wrong. There’s a step before that which nobody’s mentioned, and it’s aimed at the 15–20 automations problem specifically: you can find out which of your workflows are even capable of this failure by reading the workflow JSON, before you instrument any of them.

The assertion patterns above are right. But each one costs a node and a judgement call per workflow, and at 20 workflows across different clients that’s exactly the part that eats the time. So the useful question becomes: which ones get the assertion first?

That’s answerable from the file. For every node that writes something outside n8n — database, Sheets, CRM, Slack, any HTTP POST/PUT/DELETE — check whether there is a path from the trigger to the end of the workflow that reaches the end without passing through that node. An IF with nothing on the false branch, a Switch with no fallback, a filter that can empty the item list: each one gives you a run that completes normally and writes nothing. Those workflows are where CameronDWills’ check-in node and nathan3’s assertion earn their keep. Workflows where every write sits on every path can wait.

By hand it’s a few minutes each: list the writes, trace back to the trigger, ask whether that run would still have looked successful.

I ran that check across 2,043 public n8n templates. 13.5% had at least one high-severity case — a write that can be skipped while the run still reports success. Templates are curated and polished, so I’d read that as a floor rather than an average.

One question back, since you’re the one living it: when a client workflow goes quiet, is the delay mostly not knowing that something broke, or not knowing which of the twenty to open first? I’ve been assuming the second and I’d like to know if that’s wrong.

Disclosure: I built a free page that runs this check — paste the workflow JSON and it lists the writes that can be skipped. No login, parsed in your browser so the JSON never leaves the page: https://stillrunning.dev. Happy to walk a workflow here instead if you’d rather not paste it anywhere.

I think you are pretty much covered, but here’s what I do.

I have a systematic error handling protocol

  1. Set an error workflow → this is the absolute last resort in case everything else fails, and it should do the following:
    1. Log the error
    2. Notify the people or just you
    3. (optional but still useful) If it’s a webhook workflow, you can simply append the payload to your error db with a button to resend it to the webhook; this saves you time having to go to the executions, open the workflows, etc.. it reruns the workflow from there, provided you have idempotency keys, and it’s a transient failure.
  2. Use a good idempotency key
  3. Workflow level error
    1. Set up a payload validation system; this should solve most of your problems on empty data.
      1. Set an initial payload validation
      2. Set a final payload validation, before the write operation
    2. Never use filters; rather, use IF nodes, with strict condition validation, and a stop and error node path for everything that doesn’t correspond to these conditions
      1. :white_check_mark: This will allow you to ensure a very strict protocol from the start; everything will be thrown as error that isn’t in these conditions. You can then be proactive with the process, and expand the conditions as time goes on.
  4. Node level errors
    1. Disable continue on fail
    2. Use retry on fail
    3. In some specific cases (although very rare), use the error path.
  5. Field-level validation
    1. Process data as it arrives, convert, or clean depending on the data that arrives
    2. Use fallback value where needed: null, undefined, and others

I personally usually have a monitoring db with a state that is assigned; you can either have that as part of the data you process (the workflow process leads, and leads already have a status)

OR you can set one manually, especially if you are trying to be proactive while you permanently fix these errors as they arise by appending the run with the started status after the first payload validation and an update status after the last validation status. (This is clearly overkill for 99% of all use cases imo)

There are a few more useful things to do, but honestly this covers most of it.

The main thing, is use a systematic protocol.

Recap

  • :white_check_mark: Enforce strict rules at every level
  • :white_check_mark: Everything that isn’t what you want
  • :white_check_mark: Make sure you are notified, and you can investigate fast
  • :white_check_mark: If it wasn’t an error but a new case of data to process, increase the conditions

@CameronDWills’s inverted version is the right shape. The one thing I’d add is about the table it depends on.

Every pattern in this thread — check-in nodes, assertions, canaries — needs the workflow to cooperate. You add a node, or you write down an expectation. That’s fine when you built the thing. It’s a problem in the two cases that actually hurt: workflows you inherited, and client instances where you aren’t free to edit 20 pipelines before you know which ones matter.

There’s a signal you can get from outside without touching anything. The executions endpoint gives you, per workflow, when each run started and how it ended. Two things fall out of that history:

Cadence. Sort a workflow’s executions and take the median gap. That is the expectation — you don’t hand-write max_silence_minutes per pipeline, which at 20 workflows across clients is the same scaling problem in a new coat. A workflow that ran hourly for a month and hasn’t run in six is the “trigger stopped firing” case @Cam raised, caught without a heartbeat node.

Error rate against its own baseline, not a fixed threshold. A workflow that has always failed 2% isn’t news. One that went from 2% to 30% is. Fixed thresholds across heterogeneous workflows are, in my experience, why people mute these alerts by month two.

It doesn’t replace the assertions. The API can’t tell you a run wrote nulls — only that it ran and what it returned. Outside-in tells you a workflow went quiet or got sick; inside-out tells you it lied. You want both, but only one works on a workflow you haven’t opened yet.

(Caveat: credential expiry isn’t exposed by the API, so you catch an expired token from the 401 after the fact, not before.)

There’s a distinction buried in this thread that I think is worth separating out, because the fixes are different.

Almost everything above is about a workflow that used to work and quietly stopped — a token expired, a field got renamed, a response shape changed. The count assertions and the heartbeat workflow are the right answer to that.

The other half of this problem is workflows that never worked at all, and reported success from the very first run.

I recently executed five of my own workflows properly for the first time — against a local server standing in for the APIs, logging every outgoing request body. All five were broken, and all of them were green:

  • An HTTP Request node (typeVersion 4.2, contentType json) with the payload sitting in the body parameter instead of jsonBody. That node reads jsonBody, so what actually left the machine was {"":""}. The endpoint answered 200. Every node green.

  • Prompt text stored in a Set node without the leading =, so n8n never evaluated it. The model received the literal characters {{ $json.article_text }} and answered anyway, producing a perfectly plausible response generated from nothing.

  • A parse step that spread an API response over the item it was enriching, overwriting the original fields. Everything downstream ran happily on the wrong object.

None of these throw. An item count check does not catch them either, because the counts are all correct — one item in, one item out, at every step. What is wrong is the content of what was sent, and nothing in the execution view shows you that unless you open the node and read it.

The only thing that caught them was pointing each integration at a local server that logs the request body, running the workflow once, and reading the log. Twenty minutes to set up, and it has found something in every workflow I have pointed it at since.

So alongside “is the count what I expect”, I’d add a second question: “have I ever actually looked at what this sends?” For anything already in production, the assertions people have described above are the answer. For anything new, read the bytes once before you trust it.

@enzosoftware’s post is the one I’d build on, because “have I ever actually looked at what this sends?” is the right question, and pointing an integration at a local server that logs the request body is the cheapest way to answer it. Same experience here, it has never once come back clean.

There’s a question after that one, and I don’t think it has come up yet in the thread: once you have found the break and written a fix, how do you know the fix works?

The default answer is deploy and watch. Which makes production the test rig, for a failure class that by definition doesn’t announce itself, so “no alerts since Tuesday” is indistinguishable from “still broken, still quiet”. The cadence and error-rate baselines Miguel_Ruilope described will eventually surface it, but eventually is carrying a lot of weight there.

What made this tractable for me was keeping the recorded execution from when it broke, the actual request and response bodies rather than a summary, and replaying a candidate fix against that exact recording with egress blocked. Two properties do the work.

Sealed network: if the replay can reach the live API, you are testing today’s API, not the one that broke you, and a green result means nothing.

Raw bytes rather than a summary: the whole failure class here is shape, and the empty JSON body example above is exactly what a summarised log throws away.

Then the question is narrow enough to actually answer: does this fix turn that specific recorded failure into the right output, yes or no. Far easier to be certain about than “is this workflow healthy”.

The honest limit of it is that this only works for failures you already recorded. For anything not yet caught, @ali.alsamraay’s static read of the workflow JSON, and reading the bytes once before trusting a new workflow, are strictly better, because neither needs the failure to have happened first. Outside-in, inside-out and replay are answering three different questions, and I don’t think any one of them substitutes for the others.

Disclosure, in the same spirit as ali’s: I built a tool that does the replay-and-verify part, and its main design rule is that it refuses to claim a fix works when it cannot prove it. It is free and has no users yet, so please treat it as an approach worth stealing rather than a recommendation. The sealed-replay idea is worth more than my implementation of it.