This workflow corresponds to n8n.io template #13290 — we link there as the canonical source.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Monitor - Scheduled Workflow Health",
"nodes": [
{
"name": "Test Run",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-464,
-304
],
"parameters": {},
"typeVersion": 1
},
{
"name": "Daily Check at 9am",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-464,
-496
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours"
}
]
}
},
"typeVersion": 1.2
},
{
"name": "Fetch All Active Workflows",
"type": "n8n-nodes-base.n8n",
"position": [
-224,
-400
],
"parameters": {
"filters": {},
"requestOptions": {}
},
"typeVersion": 1
},
{
"name": "Discover Scheduled Workflows",
"type": "n8n-nodes-base.code",
"position": [
32,
-400
],
"parameters": {
"jsCode": "// ============================================================\n// CONFIG \u2014 Adjust these values to fit your setup\n// ============================================================\n\n// Optional: restrict to a specific n8n project ID.\n// Leave empty to monitor ALL active workflows.\nconst PROJECT_ID = '';\n\n// Tag name to exclude workflows from monitoring.\nconst SKIP_TAG = 'skip-monitoring';\n\n// ============================================================\n// AUTO-DISCOVERY \u2014 No changes needed below\n// ============================================================\n\nconst workflows = $input.all().map(i => i.json);\nconst results = [];\n\nfor (const wf of workflows) {\n if (wf.isArchived) continue;\n\n // Project filter\n if (PROJECT_ID) {\n const wfProject = wf.shared?.[0]?.projectId;\n if (wfProject !== PROJECT_ID) continue;\n }\n\n // Tag filter\n const tags = (wf.tags || []).map(t => (t.name || t).toString().toLowerCase());\n if (tags.includes(SKIP_TAG)) continue;\n\n // Find trigger nodes\n const nodes = wf.nodes || [];\n const scheduleTrigger = nodes.find(n => n.type === 'n8n-nodes-base.scheduleTrigger');\n const pollingTrigger = nodes.find(n =>\n n.type.includes('Trigger') &&\n n.type !== 'n8n-nodes-base.manualTrigger' &&\n n.type !== 'n8n-nodes-base.executeWorkflowTrigger' &&\n n.type !== 'n8n-nodes-base.errorTrigger' &&\n n.type !== 'n8n-nodes-base.scheduleTrigger' &&\n n.parameters?.pollTimes\n );\n\n if (!scheduleTrigger && !pollingTrigger) continue;\n\n // Calculate max allowed age before alerting\n let maxAgeHours = 48; // safe default\n\n if (scheduleTrigger) {\n const interval = scheduleTrigger.parameters?.rule?.interval?.[0];\n if (interval) {\n if (interval.field === 'cronExpression' && interval.expression) {\n maxAgeHours = parseCronMaxAge(interval.expression);\n } else if (interval.field === 'minutes') {\n const mins = interval.minutesInterval || 30;\n maxAgeHours = Math.max(2, Math.ceil(mins * 8 / 60));\n } else if (interval.field === 'hours') {\n maxAgeHours = 48;\n } else if (interval.field === 'weeks') {\n maxAgeHours = 192; // 8 days\n } else if (interval.field === 'months') {\n maxAgeHours = 840; // 35 days\n } else {\n maxAgeHours = 48;\n }\n }\n }\n\n results.push({ json: { workflowId: wf.id, name: wf.name, maxAgeHours } });\n}\n\nfunction parseCronMaxAge(expr) {\n const parts = expr.trim().split(/\\s+/);\n if (parts.length < 5) return 48;\n const [min, hour, dom, month, dow] = parts;\n\n // Monthly (e.g. \"0 9 1 * *\")\n if (dom !== '*' && !dom.startsWith('*/')) return 840;\n // Weekday-only (e.g. \"0 8,13,18 * * 1-5\") \u2014 48h covers weekends\n if (dow !== '*') return 48;\n // Every N days (e.g. \"0 9 */2 * *\")\n if (dom.startsWith('*/')) {\n const n = parseInt(dom.split('/')[1]) || 1;\n return Math.max(48, n * 36);\n }\n // Sub-hourly (e.g. \"*/15 8-17 * * *\")\n if (min.startsWith('*/')) return 48;\n // Daily\n return 48;\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"name": "Get Latest Execution",
"type": "n8n-nodes-base.n8n",
"onError": "continueRegularOutput",
"position": [
272,
-400
],
"parameters": {
"limit": 1,
"filters": {
"workflowId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.workflowId }}"
}
},
"options": {},
"resource": "execution",
"requestOptions": {}
},
"typeVersion": 1,
"alwaysOutputData": true
},
{
"name": "Check for Stale Workflows",
"type": "n8n-nodes-base.code",
"position": [
512,
-400
],
"parameters": {
"jsCode": "const executions = $input.all().map(i => i.json);\nconst config = $('Discover Scheduled Workflows').all().map(i => i.json);\n\n// Build lookup: workflowId -> latest execution\nconst execMap = {};\nfor (const exec of executions) {\n if (exec.workflowId) execMap[exec.workflowId] = exec;\n}\n\nconst stale = [];\nconst ok = [];\n\nfor (const c of config) {\n const exec = execMap[c.workflowId];\n if (!exec || !exec.startedAt) {\n stale.push({ name: c.name, reason: 'No execution found' });\n } else {\n const ageHours = (Date.now() - new Date(exec.startedAt).getTime()) / (1000 * 60 * 60);\n if (ageHours > c.maxAgeHours) {\n stale.push({\n name: c.name,\n reason: `Last ran ${Math.round(ageHours)}h ago (max: ${c.maxAgeHours}h)`,\n lastRun: exec.startedAt\n });\n } else {\n ok.push(c.name);\n }\n }\n}\n\nreturn [{ json: { stale, staleCount: stale.length, okCount: ok.length, checkedCount: config.length } }];"
},
"typeVersion": 2
},
{
"name": "Any Stale?",
"type": "n8n-nodes-base.if",
"position": [
752,
-400
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.staleCount }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2
},
{
"name": "Alert \u2014 Workflows Missed Schedule",
"type": "n8n-nodes-base.stopAndError",
"position": [
992,
-496
],
"parameters": {
"errorMessage": "={{ $json.staleCount }} scheduled workflow(s) missed their schedule:\n{{ $json.stale.map(s => '\u2022 ' + s.name + ' \u2014 ' + s.reason).join('\\n') }}"
},
"typeVersion": 1
},
{
"name": "All Healthy",
"type": "n8n-nodes-base.noOp",
"position": [
992,
-304
],
"parameters": {},
"typeVersion": 1
},
{
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1072,
-752
],
"parameters": {
"color": 4,
"width": 524,
"height": 798,
"content": "## Monitor \u2014 Scheduled Workflow Health\n\nAutomatically detects when scheduled or polling-trigger workflows stop running. No hardcoded config needed \u2014 it auto-discovers all active scheduled workflows and calculates expected run frequency from their trigger settings.\n\n### How it works\n1. Fetches all active workflows via the n8n API\n2. Filters to those with a Schedule Trigger or polling trigger (e.g. Outlook, Gmail, Google Sheets)\n3. Parses cron expressions and interval settings to calculate max allowed age\n4. Fetches the latest execution for each and flags any that are overdue\n5. Raises an error for stale workflows \u2014 pair with an Error Workflow for Slack/email alerts\n\n### Setup steps\n1. Add your **n8n API credential** to the two n8n nodes\n2. Set this workflow as **not monitored** by adding a `skip-monitoring` tag to it\n3. Optionally set `PROJECT_ID` in the \"Discover Scheduled Workflows\" code node to limit to one project\n4. Connect an **Error Workflow** (in workflow settings) to get notified on failures\n5. Activate the workflow\n\n### Customization\n- Change the schedule (default: daily 9am)\n- Change `SKIP_TAG` to use a different tag name for excluding workflows\n- The max age calculation adds generous margins (e.g. 48h for daily workflows to survive weekends)"
},
"typeVersion": 1
},
{
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-272,
-480
],
"parameters": {
"width": 480,
"height": 260,
"content": "## Discovery"
},
"typeVersion": 1
},
{
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
224,
-480
],
"parameters": {
"width": 720,
"height": 260,
"content": "## Staleness Check"
},
"typeVersion": 1
},
{
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
960,
-544
],
"parameters": {
"width": 480,
"height": 380,
"content": "## Alert or OK"
},
"typeVersion": 1
}
],
"settings": {
"executionOrder": "v1"
},
"connections": {
"Test Run": {
"main": [
[
{
"node": "Fetch All Active Workflows",
"type": "main",
"index": 0
}
]
]
},
"Any Stale?": {
"main": [
[
{
"node": "Alert \u2014 Workflows Missed Schedule",
"type": "main",
"index": 0
}
],
[
{
"node": "All Healthy",
"type": "main",
"index": 0
}
]
]
},
"Daily Check at 9am": {
"main": [
[
{
"node": "Fetch All Active Workflows",
"type": "main",
"index": 0
}
]
]
},
"Get Latest Execution": {
"main": [
[
{
"node": "Check for Stale Workflows",
"type": "main",
"index": 0
}
]
]
},
"Check for Stale Workflows": {
"main": [
[
{
"node": "Any Stale?",
"type": "main",
"index": 0
}
]
]
},
"Fetch All Active Workflows": {
"main": [
[
{
"node": "Discover Scheduled Workflows",
"type": "main",
"index": 0
}
]
]
},
"Discover Scheduled Workflows": {
"main": [
[
{
"node": "Get Latest Execution",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Automatically detect when your scheduled or polling-trigger workflows stop running. Unlike error handlers that catch failures when workflows execute, this catches the silent killer: workflows that simply never trigger at all — broken schedules, accidental deactivation, or…
Source: https://n8n.io/workflows/13290/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Retry Execution Hourly. Uses manualTrigger, n8n, httpRequest, noOp. Event-driven trigger; 13 nodes.
Automatically backs up all workflows to Google Drive daily. Triggers daily at 11 PM (or manually on demand) Creates a timestamped backup folder in Google Drive Fetches all workflows from your n8n inst
Schedule Filter. Uses manualTrigger, n8n, splitInBatches, googleDrive. Event-driven trigger; 10 nodes.
Manual Schedule. Uses manualTrigger, n8n, scheduleTrigger, noOp. Event-driven trigger; 7 nodes.
Auto - Resume Disabled Workflows. Uses manualTrigger, n8n, scheduleTrigger. Event-driven trigger; 5 nodes.