DEV Community

AI Automation Playbook
AI Automation Playbook

Posted on

Your n8n Workflow Never Started: Build a Dead-Man's Switch That Notices

Your n8n Workflow Never Started: Build a Dead-Man's Switch That Notices

The gap Error Trigger cannot cover

n8n's Error Trigger only fires when a workflow run fails. A workflow that never starts — a dead cron, a changed webhook URL, a deactivated workflow after an edit — produces no run, no failure, and no alert. Nothing tells you. I covered the "failed silently" side in a recent Short; this is the other side: the "never ran at all" side.

The fix is a dead-man's switch: a small watchdog workflow that checks the executions list itself, on a schedule, and alerts you when the monitored workflow has gone quiet for too long. "No run at all" becomes the alert condition.

How it works

One watchdog workflow, five working nodes plus setup:

  1. Schedule (hourly) — the heartbeat. Set the interval shorter than your alert threshold.
  2. CONFIG — four values you paste in: your n8n base URL, the monitored workflow's ID, the threshold in minutes, and a Public API key.
  3. Get Last Executions — calls the n8n Public API (/api/v1/executions?workflowId=...&limit=1) and pulls the most recent run.
  4. Check Last Run — a Code node that computes minutes since the last run. Zero executions at all returns overdue: true with reason NO_RUNS_FOUND; a run older than the threshold returns OVERDUE; anything fresher returns HEALTHY.
  5. IF Overdue? — true branch sends the alert, false branch does nothing.
  6. ALERT (Gmail) — sends one email: which workflow, why it fired, last run time, and age vs. threshold.

Setup (2 minutes, plus one Gmail credential)

  1. In n8n, go to Settings > n8n API and create a Public API key with scopes workflow:list + execution:list. Paste it into CONFIG.apiKey.
  2. Open the workflow you want to watch, copy its ID from the URL, and paste it into CONFIG.monitoredWorkflowId.
  3. Set CONFIG.thresholdMinutes to roughly 2x the monitored workflow's expected run interval. A workflow that should run every hour gets a 120-minute threshold; a daily one gets a day-plus margin. Too tight and you get noise; too loose and the alert arrives late.
  4. Create a Gmail OAuth2 credential in n8n, select it on the ALERT node, and paste your alert inbox into CONFIG.alertEmail.
  5. Activate the watchdog, then test it: point it at a workflow you know is stale and confirm the alert arrives before you rely on it.

What I verified, and what I did not claim

  • Detection logic: proven live, both branches — run against a local n8n instance (v2.44.5), a workflow with no executions fired the alert branch, and a workflow with a run 3 minutes old returned HEALTHY against a 120-minute threshold.
  • Delivery: the Gmail node is wired per the setup above. Run one test alert and confirm receipt yourself before relying on it — this article does not claim an alert email was received yet.

  • Detection logic: proven live, both branches.

  • Delivery: the Gmail node is wired per the setup above. Run one test alert and confirm receipt yourself — the same instruction applies to me before this article claims a received alert.

  • This watchdog is a separate setup. It is not part of the free starter pack or the paid workflow packs, and neither pack contains an Error Trigger node — so this article does not claim any pack fixes this gap.

The workflow JSON

Paste this into n8n via Import > From file, then fill in CONFIG and the Gmail credential. All secrets are placeholders — nothing real is in the file.

{
  "active": false,
  "connections": {
    "CONFIG - Watchdog": {
      "main": [
        [
          {
            "index": 0,
            "node": "Get Last Executions",
            "type": "main"
          }
        ]
      ]
    },
    "Check Last Run": {
      "main": [
        [
          {
            "index": 0,
            "node": "IF - Overdue?",
            "type": "main"
          }
        ]
      ]
    },
    "Get Last Executions": {
      "main": [
        [
          {
            "index": 0,
            "node": "Check Last Run",
            "type": "main"
          }
        ]
      ]
    },
    "IF - Overdue?": {
      "main": [
        [
          {
            "index": 0,
            "node": "ALERT - Send Gmail",
            "type": "main"
          }
        ],
        [
          {
            "index": 0,
            "node": "OK - No alert needed",
            "type": "main"
          }
        ]
      ]
    },
    "Schedule - Every hour": {
      "main": [
        [
          {
            "index": 0,
            "node": "CONFIG - Watchdog",
            "type": "main"
          }
        ]
      ]
    }
  },
  "id": "watchdog-dead-mans-switch-v1",
  "name": "[APB] Watchdog - Dead-man's switch for a workflow that never runs",
  "nodes": [
    {
      "id": "sched-001",
      "name": "Schedule - Every hour",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      },
      "position": [
        240,
        300
      ],
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2
    },
    {
      "id": "config-001",
      "name": "CONFIG - Watchdog",
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "baseUrl",
              "type": "string",
              "value": "http://localhost:5678"
            },
            {
              "id": "a2",
              "name": "monitoredWorkflowId",
              "type": "string",
              "value": "PASTE_MONITORED_WORKFLOW_ID_HERE"
            },
            {
              "id": "a3",
              "name": "thresholdMinutes",
              "type": "number",
              "value": 120
            },
            {
              "id": "a4",
              "name": "apiKey",
              "type": "string",
              "value": "PASTE_N8N_PUBLIC_API_KEY_HERE"
            },
            {
              "id": "a5",
              "name": "alertEmail",
              "type": "string",
              "value": "PASTE_ALERT_EMAIL_HERE"
            }
          ]
        },
        "values": {
          "number": [],
          "string": []
        }
      },
      "position": [
        460,
        300
      ],
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4
    },
    {
      "id": "http-001",
      "name": "Get Last Executions",
      "parameters": {
        "headerParameters": {
          "parameters": [
            {
              "name": "X-N8N-API-KEY",
              "value": "={{ $json.apiKey }}"
            }
          ]
        },
        "method": "GET",
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "url": "={{ $json.baseUrl }}/api/v1/executions?workflowId={{ $json.monitoredWorkflowId }}&limit=1"
      },
      "position": [
        700,
        300
      ],
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2
    },
    {
      "id": "code-001",
      "name": "Check Last Run",
      "parameters": {
        "jsCode": "const executions = $('Get Last Executions').first().json.data || [];\nconst cfg = $('CONFIG - Watchdog').first().json;\nconst threshold = cfg.thresholdMinutes || 120;\nif (executions.length === 0) {\n  return [{ json: { monitoredWorkflowId: cfg.monitoredWorkflowId, lastRunAt: null, ageMinutes: null, overdue: true, reason: 'NO_RUNS_FOUND' } }];\n}\nconst last = executions[0];\nconst startedAt = last.startedAt;\nconst ageMinutes = Math.round((Date.now() - new Date(startedAt).getTime()) / 60000);\nreturn [{ json: { monitoredWorkflowId: cfg.monitoredWorkflowId, lastRunAt: startedAt, lastStatus: last.status, ageMinutes, thresholdMinutes: threshold, overdue: ageMinutes > threshold, reason: ageMinutes > threshold ? 'OVERDUE' : 'HEALTHY' } }];"
      },
      "position": [
        940,
        300
      ],
      "type": "n8n-nodes-base.code",
      "typeVersion": 2
    },
    {
      "id": "if-001",
      "name": "IF - Overdue?",
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.overdue }}",
              "operator": {
                "name": "filter.boolean.true",
                "operation": "true",
                "type": "boolean"
              },
              "rightValue": true
            }
          ],
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          }
        },
        "looseTypeValidation": true
      },
      "position": [
        1180,
        300
      ],
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2
    },
    {
      "id": "gmail-001",
      "name": "ALERT - Send Gmail",
      "parameters": {
        "resource": "message",
        "operation": "send",
        "subject": "=\ud83d\udea8 Watchdog: workflow {{ $json.monitoredWorkflowId }} has not run",
        "emailType": "text",
        "message": "=Workflow {{ $json.monitoredWorkflowId }} has not run.\nReason: {{ $json.reason }}\nLast run: {{ $json.lastRunAt }}\nAge: {{ $json.ageMinutes }} minutes (threshold {{ $json.thresholdMinutes }}).\n\nThis alert comes from the dead-man's switch watchdog. Error Trigger cannot catch this case: a workflow that never starts produces no run and no failure.",
        "sendTo": "= {{ $('CONFIG - Watchdog').first().json.alertEmail }}",
        "options": {}
      },
      "position": [
        1420,
        180
      ],
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "credentials": {
        "gmailOAuth2": {
          "id": "",
          "name": "YOUR Gmail OAuth2"
        }
      }
    },
    {
      "id": "noop-002",
      "name": "OK - No alert needed",
      "parameters": {},
      "position": [
        1420,
        420
      ],
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1
    },
    {
      "id": "sticky-001",
      "name": "Sticky - Setup",
      "parameters": {
        "color": 4,
        "content": "## Dead-man's switch: setup (2 minutes)\n1. In n8n Settings > n8n API, create a Public API key with scopes workflow:list + execution:list. Paste it into CONFIG.apiKey.\n2. Paste the monitored workflow's ID into CONFIG.monitoredWorkflowId (open the workflow, copy the ID from its URL).\n3. Set CONFIG.thresholdMinutes to ~2x the monitored workflow's expected run interval.\n4. Connect Gmail: create a Gmail OAuth2 credential in n8n and select it on the ALERT node, then paste your alert inbox into CONFIG.alertEmail. Send one test alert and confirm it arrives before relying on it. The detection logic is already verified in local n8n; this Gmail node is the delivery channel.\n\nWhy this works where Error Trigger cannot: Error Trigger only fires when a run FAILS. A workflow that never starts produces no run, no failure, and no alert. This watchdog checks the executions list itself, so 'no run at all' is itself the alert condition.",
        "height": 320,
        "width": 620
      },
      "position": [
        240,
        40
      ],
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1
    }
  ],
  "settings": {
    "executionOrder": "v1"
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this instead of just checking manually

Manual checks work until they don't — the week you forget is the week the workflow dies. The watchdog costs one hourly API call and one email only when something is actually wrong. For anything that touches customers (missed-call text-backs, review requests, invoice runs), "nobody noticed for days" is the expensive failure mode, and this is the cheapest way to close it.


I'm a verified creator on the official n8n template library — 7 templates live: workflow-02, workflow-03, workflow-04, workflow-07, OB-01, workflow-01, workflow-11 (https://n8n.io/workflows/20250, /20580, /20743, /20850, /21126, /21293, /21618). Free starter pack: https://automateplaybook.gumroad.com/l/hhnxv

Top comments (0)