Case studies
CASE STUDY — Operations

Catching the workflow that quietly stopped — when n8n says success but nothing happened

Deployed at Automation operations / All teams4 days to build
n8nPostgresSlackn8n Schedule

The problem wasn't that no alert came. The problem was that no alert looked exactly like everything working. The execution log stayed green for three weeks while nothing at all happened.

Once an automation is built, attention moves to the next one — and silence starts reading as "still running fine." But automations rarely die loudly. An API changes its response shape and the filter stops matching anything. A sheet column gets renamed by one character. A cron quietly deactivates. Every one of those is recorded as a success.

workflow-watchdog.workflowLIVE
CRHourlyPGRead run history!Detect silence / zeroSLSlack alert

Why error alerts aren't enough

n8n's Error Trigger is good, but it only catches executions that failed. In practice automations die in three ways, and two of them never reach an Error Trigger.

  • Failed — the execution ended in an error. Error Trigger catches this. It's the problem that's already solved.
  • Zero results — the execution succeeded but processed nothing. A filter condition no longer matches, the source data changed shape, or a query returned an empty set. From n8n's point of view this run finished without incident.
  • Never ran — the workflow didn't fire at all. The cron was switched off, the instance came back from a restart deactivated, or the trigger never fired. There is no execution to alert on, so no error alert can possibly be produced. This is the one that goes unnoticed longest.
The third case is the crux. A failure means "something happened and it went wrong." Silence means "nothing happened at all." The second can never be detected from inside the workflow — code that didn't run cannot report that it didn't run. That's why a second workflow has to watch from the outside.

How it works

  1. Every watched workflow writes one row per run into a `workflow_runs` table — its name, timestamp, status, and how many items it actually processed. Adding a single Postgres node at the end of the workflow is the entire change.
  2. A Schedule Trigger node wakes the watchdog every hour on the hour.
  3. A Postgres node fetches, in one query, the most recent run for each workflow and how many of its last three successful runs returned zero results.
  4. A Code node evaluates three conditions — did the last run error (failed), has no run been recorded for more than twice the expected interval (silent), were the last three successful runs all zero (zeroed).
  5. If nothing is wrong, the Code node returns an empty array and the workflow ends quietly. The Slack node never executes. Getting no message when everything is healthy is the whole point of the design.
  6. If something is wrong, all findings are grouped into a single Slack message, one line per workflow with an icon per failure type, posted to #automation-alerts.

What you need

  • n8n — self-hosted or Cloud. No community nodes required. This uses four core nodes only: Schedule Trigger, Postgres, Code, Slack.
  • One Postgres database — you don't need a new one. Add a single table to a database you already run. Only names, timestamps, and counts are stored, so it stays tiny.
  • A Postgres credential — n8n → Credentials → Postgres. Host, port (5432 by default), database, user, password. If n8n runs in Docker and your database runs on the host machine, the host must be host.docker.internal, not localhost. This is where beginners get stuck most often.
  • A Slack Bot User OAuth Token — create a Slack app, grant the chat:write and channels:read scopes, install it to your workspace, then register the token (it starts with xoxb-) in n8n → Credentials → Slack API.
  • A Slack channel for alerts — e.g. #automation-alerts. Run /invite @your-bot-name in it, or posting fails with not_in_channel.
  • A list of workflows to watch, each with its expected interval in minutes — 60 for an hourly workflow, 1440 for a daily one. This number is what silence detection measures against.

Step 1 — create the run-history table

This is where watched workflows record their runs. Run the following as-is in psql, DBeaver, or any client. Six columns, nothing more.

CREATE TABLE IF NOT EXISTS workflow_runs (
  id                    bigserial   PRIMARY KEY,
  workflow_name         text        NOT NULL,  -- match the name shown in n8n
  run_at                timestamptz NOT NULL DEFAULT now(),
  status                text        NOT NULL,  -- 'success' or 'error'
  result_count          integer     NOT NULL DEFAULT 0,  -- items this run actually processed
  expected_interval_min integer     NOT NULL   -- expected cadence in minutes; 60 = hourly
);

-- Keeps lookups fast as rows accumulate.
CREATE INDEX IF NOT EXISTS workflow_runs_name_time_idx
  ON workflow_runs (workflow_name, run_at DESC);
result_count is the most important column in this entire case study. Because it records how many items were processed rather than merely that the run succeeded, zero-result failures become visible. Read three emails, store 3. Read none, store 0.

Step 2 — add the recording node to each watched workflow

For every workflow you want watched, append a single `Postgres` node at the very end. Leave the operation on Execute Query and paste the query below, changing daily-sales-report and 60 to match that workflow.

INSERT INTO workflow_runs
  (workflow_name, status, result_count, expected_interval_min)
VALUES
  ('daily-sales-report', 'success', {{ $items().length }}, 60);

{{ $items().length }} is the number of items that reached this node — in other words, what this run actually processed. If you count throughput some other way, substitute your own value. To record failures too, add an Error Trigger to the workflow with a second node that inserts the same row with status set to 'error'.

Step 3 — the detection query

This is what sits in the watchdog's Read Run History node. It already ships inside the downloadable JSON, so you can leave it alone — read on only if you want to see how the judgement is made. It returns exactly one row per workflow.

WITH latest AS (
  -- Most recent run per workflow
  SELECT DISTINCT ON (workflow_name)
         workflow_name, run_at, status, result_count, expected_interval_min
  FROM workflow_runs
  ORDER BY workflow_name, run_at DESC
),
last_three AS (
  -- How many of the last three successful runs returned zero
  SELECT workflow_name,
         count(*) FILTER (WHERE result_count = 0) AS zero_runs,
         count(*)                                 AS sampled_runs
  FROM (
    SELECT workflow_name, result_count,
           row_number() OVER (PARTITION BY workflow_name ORDER BY run_at DESC) AS rn
    FROM workflow_runs
    WHERE status = 'success'
  ) s
  WHERE rn <= 3
  GROUP BY workflow_name
)
SELECT l.workflow_name,
       l.run_at,
       l.status,
       l.result_count,
       l.expected_interval_min,
       round(EXTRACT(EPOCH FROM (now() - l.run_at)) / 60)::int AS minutes_since_last_run,
       COALESCE(t.zero_runs, 0)    AS zero_runs_in_last_3,
       COALESCE(t.sampled_runs, 0) AS sampled_runs
FROM latest l
LEFT JOIN last_three t ON t.workflow_name = l.workflow_name
ORDER BY l.workflow_name;
⬇︎ Download the workflow (workflow-watchdog.json)
Add one Postgres credential, one Slack credential, and your channel name — that's all it takes to run. The Sticky Note at the top of the workflow includes the table-creation SQL.
Being precise about what was verified. The detection query was executed on PostgreSQL 17 against fixtures covering all four states (healthy / never ran / three consecutive zero-result runs / last run errored) and its output confirmed. The Code node logic was then executed in Node.js against that query's real output, confirming it flags all three failure modes correctly and returns an empty array when everything is healthy. The workflow JSON uses the same node type and version combination as our already-published workflows (scheduleTrigger 1.2 / postgres 2.5 / code 2 / slack 2.3). Posting to a live Slack workspace was not verified at the time of writing. Last verified: 2026-08-15.

Setup (25 minutes)

  1. Create the table — run the Step 1 SQL against your database. You should see CREATE TABLE followed by CREATE INDEX twice.
  2. Register the Postgres credential — n8n sidebar → Credentials → Add credential → search Postgres → fill in host, port, database, user, password → click Test at the top right. Wait for the green check before moving on. A failure here is almost always the host value (see "What you need" above).
  3. Create the Slack app — api.slack.com/apps → Create New App → From scratch → name it and pick your workspace → OAuth & Permissions in the sidebar → under Bot Token Scopes add chat:write and channels:readInstall to Workspace at the top → copy the token beginning xoxb-.
  4. Register the Slack credential — n8n → Credentials → Add credential → Slack API → paste the token → Test.
  5. Invite the bot — open your alert channel in Slack and type /invite @your-bot-name. Skip this and you'll hit not_in_channel later.
  6. Import the workflow — download the JSON above, then n8n → Workflows → ... at the top right → Import from File.
  7. Set your channel — open the Post to #automation-alerts node and replace REPLACE_WITH_YOUR_CHANNEL_NAME_OR_ID with your channel name (e.g. automation-alerts, no #).
  8. Attach the credentials — open Read Run History and Post to #automation-alerts and pick the credentials you just created from each Credential dropdown.
  9. Add the recording nodes — open each workflow you want watched and append the Step 2 Postgres node, with that workflow's name and interval.
  10. Test — hit Execute Workflow on the watchdog. With no history recorded yet, nothing happening is the correct result. To see an actual alert, do the next step.
  11. Trigger an alert on purpose — run INSERT INTO workflow_runs (workflow_name, run_at, status, result_count, expected_interval_min) VALUES ('test-wf', now() - interval '500 minutes', 'success', 3, 60); then hit Execute Workflow again. A silence alert should appear in Slack. Delete the row afterwards.
  12. Activate — flip the Activate toggle at the top right. It now checks every hour on the hour.

Tuning the thresholds

Two constants sit at the top of the Detect Silence + Zero Runs node. In practice these are the only things you'll ever adjust.

  • `SILENCE_TOLERANCE` (default 2) — how many times the expected interval may elapse before a workflow counts as silent. At 2, an hourly workflow is flagged after roughly two hours without a run. Set it too low and ordinary scheduling jitter pages you; too high and discovery is slow. Start at 2 and move to 3 if alerts feel noisy.
  • `ZERO_RUN_STREAK` (default 3) — how many consecutive zero-result runs constitute a problem. For workflows where zero is genuinely normal some days (no weekend orders, say), raise it to 5 or more.

What happens in edge cases

  • Everything healthy — the Code node returns an empty array and the Slack node never runs. Zero alert noise. This should be your default state.
  • What if the watchdog itself dies? — a genuine limitation of this design; nobody watches the watcher. In production, have the watchdog ping a free external health-check service (healthchecks.io and similar) every hour and email you when the pings stop. Without that one addition, everything else here can quietly become meaningless.
  • Days when zero is normal — some workflows legitimately process nothing on weekends and holidays. Raise ZERO_RUN_STREAK, or exclude those workflow names inside the Code node.
  • Workflows that run more often than the hourly check — a workflow running every five minutes can go up to an hour before its silence is noticed. If that's too slow, change the Schedule Every Hour cron to 0 */10 * * * * for a ten-minute cadence.
  • Slack being down — the alert is simply lost. For anything critical, chain an email node after the Slack node for redundancy.
  • The table growing forever — the index keeps queries fast even at thousands of rows a day, but old rows are worth pruning. Put DELETE FROM workflow_runs WHERE run_at < now() - interval '90 days'; in a monthly workflow.
3
failure modes detected
Hourly
check cadence
Zero
alerts when healthy
25 min
setup time
A workflow can show as successful while still producing no useful result.n8n community forum

Your operation belongs
in here, too.

Tell us the most repetitive task you have. We'll map an automation scenario for it.