{
  "name": "n8n workflow health watchdog",
  "nodes": [
    {
      "id": "n1-schedule",
      "name": "Every 30 minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, 300],
      "parameters": {
        "rule": { "interval": [{ "field": "minutes", "minutesInterval": 30 }] }
      }
    },
    {
      "id": "n2-config",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [220, 300],
      "parameters": {
        "assignments": {
          "assignments": [
            { "id": "c1", "name": "n8nBaseUrl", "value": "http://localhost:5678", "type": "string" },
            { "id": "c2", "name": "workflowIds", "value": "REPLACE_WITH_ID_1,REPLACE_WITH_ID_2", "type": "string" },
            { "id": "c3", "name": "maxSilenceMinutes", "value": 30, "type": "number" },
            { "id": "c4", "name": "alertWebhookUrl", "value": "https://hooks.example.com/REPLACE_ME", "type": "string" },
            { "id": "c5", "name": "alertField", "value": "text", "type": "string" },
            { "id": "c6", "name": "autoReactivate", "value": false, "type": "boolean" }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "n3-split",
      "name": "Split IDs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [440, 300],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const c = $('Config').first().json;\nconst ids = String(c.workflowIds).split(',').map(s => s.trim()).filter(Boolean);\nreturn ids.map(id => ({ json: {\n  id,\n  n8nBaseUrl: String(c.n8nBaseUrl).replace(/\\/+$/, ''),\n  maxSilenceMinutes: Number(c.maxSilenceMinutes),\n  alertWebhookUrl: c.alertWebhookUrl,\n  alertField: c.alertField || 'text',\n  autoReactivate: c.autoReactivate === true || c.autoReactivate === 'true',\n} }));"
      }
    },
    {
      "id": "n4-workflow",
      "name": "Get workflow",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [660, 300],
      "parameters": {
        "method": "GET",
        "url": "={{ $('Split IDs').item.json.n8nBaseUrl }}/api/v1/workflows/{{ $('Split IDs').item.json.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": { "id": "REPLACE_AFTER_IMPORT", "name": "n8n API key header" }
      }
    },
    {
      "id": "n5-success",
      "name": "Last success",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [880, 300],
      "parameters": {
        "method": "GET",
        "url": "={{ $('Split IDs').item.json.n8nBaseUrl }}/api/v1/executions?workflowId={{ $('Split IDs').item.json.id }}&status=success&limit=1",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": { "id": "REPLACE_AFTER_IMPORT", "name": "n8n API key header" }
      }
    },
    {
      "id": "n6-errors",
      "name": "Recent errors",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1100, 300],
      "parameters": {
        "method": "GET",
        "url": "={{ $('Split IDs').item.json.n8nBaseUrl }}/api/v1/executions?workflowId={{ $('Split IDs').item.json.id }}&status=error&limit=5",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": { "id": "REPLACE_AFTER_IMPORT", "name": "n8n API key header" }
      }
    },
    {
      "id": "n7-verdict",
      "name": "Verdict",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1320, 300],
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "const cfgs = $('Split IDs').all();\nconst wfs = $('Get workflow').all();\nconst oks = $('Last success').all();\nconst errs = $('Recent errors').all();\nconst now = Date.now();\nconst ts = (s) => Date.parse(s);\nconst newest = (resp) => {\n  const items = ((resp && resp.data) || []).filter(e => e.startedAt);\n  return items.reduce((a, b) => (!a || ts(b.startedAt) > ts(a.startedAt)) ? b : a, null);\n};\nconst finish = (e) => ts(e.stoppedAt || e.startedAt);\n\nreturn cfgs.map((c, i) => {\n  const cfg = c.json;\n  const wf = wfs[i].json;\n  const okResp = oks[i].json;\n  const errResp = errs[i].json;\n  const name = wf.name || wf.id;\n  const lastOk = newest(okResp);\n  const lastErr = newest(errResp);\n  const errList = (errResp && errResp.data) || [];\n  let minutes = null;\n  let errorsSince = errList.length;\n  if (lastOk) {\n    const okTime = finish(lastOk);\n    minutes = Math.floor((now - okTime) / 60000 + 0.5);\n    errorsSince = errList.filter(e => e.startedAt && ts(e.startedAt) > okTime).length;\n  }\n  let verdict, detail;\n  if (!wf.active) { verdict = 'INACTIVE'; detail = 'workflow is not active'; }\n  else if (minutes === null) { verdict = 'STALE'; detail = 'no successful execution found'; }\n  else if (minutes > cfg.maxSilenceMinutes) { verdict = 'STALE'; detail = `last success ${minutes} min ago (limit ${cfg.maxSilenceMinutes})`; }\n  else if (lastErr && ts(lastErr.startedAt) > finish(lastOk)) { verdict = 'ERRORING'; detail = `last success ${minutes} min ago`; }\n  else { verdict = 'OK'; detail = `last success ${minutes} min ago`; }\n  if ((verdict === 'STALE' || verdict === 'ERRORING') && errorsSince) detail += `, ${errorsSince} error(s) since`;\n  return { json: {\n    id: cfg.id,\n    name,\n    active: !!wf.active,\n    verdict,\n    minutesSinceSuccess: minutes,\n    recentErrors: errList.length,\n    errorsSinceSuccess: errorsSince,\n    message: `[n8n watchdog] ${name} (${cfg.id}): ${verdict} - ${detail}`,\n    n8nBaseUrl: cfg.n8nBaseUrl,\n    alertWebhookUrl: cfg.alertWebhookUrl,\n    alertField: cfg.alertField,\n    autoReactivate: cfg.autoReactivate,\n  } };\n});"
      }
    },
    {
      "id": "n8-ifbad",
      "name": "Not OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [1540, 200],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            {
              "id": "if1",
              "leftValue": "={{ $json.verdict }}",
              "rightValue": "OK",
              "operator": { "type": "string", "operation": "notEquals" }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "n9-alert",
      "name": "Send alert",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1760, 200],
      "parameters": {
        "method": "POST",
        "url": "={{ $json.alertWebhookUrl }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ [$json.alertField]: $json.message }) }}",
        "options": {}
      }
    },
    {
      "id": "n10-ifreact",
      "name": "Inactive and autoReactivate?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [1540, 420],
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 },
          "conditions": [
            {
              "id": "if2",
              "leftValue": "={{ $json.verdict }}",
              "rightValue": "INACTIVE",
              "operator": { "type": "string", "operation": "equals" }
            },
            {
              "id": "if3",
              "leftValue": "={{ $json.autoReactivate }}",
              "rightValue": "",
              "operator": { "type": "boolean", "operation": "true", "singleValue": true }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "n11-activate",
      "name": "Activate workflow",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1760, 420],
      "parameters": {
        "method": "POST",
        "url": "={{ $json.n8nBaseUrl }}/api/v1/workflows/{{ $json.id }}/activate",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "credentials": {
        "httpHeaderAuth": { "id": "REPLACE_AFTER_IMPORT", "name": "n8n API key header" }
      }
    }
  ],
  "connections": {
    "Every 30 minutes": { "main": [[{ "node": "Config", "type": "main", "index": 0 }]] },
    "Config": { "main": [[{ "node": "Split IDs", "type": "main", "index": 0 }]] },
    "Split IDs": { "main": [[{ "node": "Get workflow", "type": "main", "index": 0 }]] },
    "Get workflow": { "main": [[{ "node": "Last success", "type": "main", "index": 0 }]] },
    "Last success": { "main": [[{ "node": "Recent errors", "type": "main", "index": 0 }]] },
    "Recent errors": { "main": [[{ "node": "Verdict", "type": "main", "index": 0 }]] },
    "Verdict": {
      "main": [[
        { "node": "Not OK?", "type": "main", "index": 0 },
        { "node": "Inactive and autoReactivate?", "type": "main", "index": 0 }
      ]]
    },
    "Not OK?": { "main": [[{ "node": "Send alert", "type": "main", "index": 0 }], []] },
    "Inactive and autoReactivate?": { "main": [[{ "node": "Activate workflow", "type": "main", "index": 0 }], []] }
  },
  "settings": { "executionOrder": "v1" }
}
