{
  "id": "uRJUIbLFPc0IqnTz",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Dead Man's Switch for n8n Workflows",
  "tags": [],
  "nodes": [
    {
      "id": "08cce57e-9551-4771-a4a9-97b275a82d5c",
      "name": "Receive heartbeat ping",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -336,
        -112
      ],
      "parameters": {
        "path": "heartbeat",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "3b75082f-d922-4ab0-8245-dc87ae836ab5",
      "name": "Set receiver config",
      "type": "n8n-nodes-base.set",
      "position": [
        -144,
        -112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5e9f8320-78e0-4ba4-a2da-f677dd9ed14f",
              "name": "shared_secret",
              "type": "string",
              "value": "your-secret-here"
            }
          ]
        }
      },
      "typeVersion": 3.5
    },
    {
      "id": "1ce3054c-9d9b-4580-a4b7-5b9d0f8bea16",
      "name": "Validate shared secret",
      "type": "n8n-nodes-base.if",
      "position": [
        32,
        -112
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e660a356-21f3-4fcb-bee7-b5d11d27bebf",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $('Receive heartbeat ping').first().json.headers['x-heartbeat-secret'] }}",
              "rightValue": "={{ $('Set receiver config').first().json.shared_secret }}"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "0ac72dbf-f9ee-4dda-a8bc-de276696efb4",
      "name": "Look up job in registry",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        464,
        -128
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $('Receive heartbeat ping').first().json.body.job_id }}",
              "lookupColumn": "job_id"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7,
      "alwaysOutputData": true
    },
    {
      "id": "04104f85-1588-4fe5-8e40-5096c7fd4103",
      "name": "Is job registered?",
      "type": "n8n-nodes-base.if",
      "position": [
        624,
        -128
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "addea103-f30d-44b9-8c6a-0647bca2d383",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.job_id }}",
              "rightValue": "={{ $('Receive heartbeat ping').first().json.body.job_id }}"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "386881ae-e3de-4c11-aaa2-a111efb1c6f3",
      "name": "Record heartbeat",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1056,
        -144
      ],
      "parameters": {
        "columns": {
          "value": {
            "job_id": "={{ $('Receive heartbeat ping').first().json.body.job_id }}",
            "status": "OK",
            "last_alert_at": "={{ '' }}",
            "last_heartbeat": "={{ $now.toUTC().toISO() }}"
          },
          "schema": [
            {
              "id": "job_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "job_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "job_name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "job_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "expected_every_minutes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "expected_every_minutes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "grace_minutes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "grace_minutes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "last_heartbeat",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "last_heartbeat",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "last_alert_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "last_alert_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "active",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "active",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "alert_channel",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "alert_channel",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "job_id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "9a77fbe4-3199-4c00-aecf-f20bffb9ea11",
      "name": "Return 200 OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1552,
        -80
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ ok: true, job_id: $('Receive heartbeat ping').first().json.body.job_id, recorded_at: $now.toUTC().toISO() }) }}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "b1574e08-c3da-484c-ac7e-175bc4af63c1",
      "name": "Return 404 unknown job",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        768,
        -48
      ],
      "parameters": {
        "options": {
          "responseCode": 404
        },
        "respondWith": "text",
        "responseBody": "Unknown job_id"
      },
      "typeVersion": 1.5
    },
    {
      "id": "f69a3180-f89a-4690-8d7d-89df7ca14747",
      "name": "Return 401 unauthorized",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        208,
        -32
      ],
      "parameters": {
        "options": {
          "responseCode": 401
        },
        "respondWith": "text",
        "responseBody": "Unauthorized"
      },
      "typeVersion": 1.5
    },
    {
      "id": "a7ad71d0-4aa8-4a8c-8e36-42268403616b",
      "name": "Check every 15 minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -336,
        464
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "e9c137f2-eeae-4e94-8a93-9c9a16d473ee",
      "name": "Set checker config",
      "type": "n8n-nodes-base.set",
      "position": [
        -160,
        464
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5b9143a0-dea8-439b-a660-e85624d0e3be",
              "name": "default_grace_minutes",
              "type": "string",
              "value": "5"
            },
            {
              "id": "c033c4fd-76e9-45b6-972c-7862c3ce38c7",
              "name": "re_alert_after_minutes",
              "type": "string",
              "value": "60"
            },
            {
              "id": "4d017e2a-7fe6-48b4-9232-a6a022ab23f7",
              "name": "default_alert_channel",
              "type": "string",
              "value": "#your-alert-channel"
            }
          ]
        }
      },
      "typeVersion": 3.5
    },
    {
      "id": "5b3121ef-0449-44a1-a41d-525e21bb788a",
      "name": "Get all monitored jobs",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        16,
        464
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "117e5310-c157-435d-a437-3f31c6e3016f",
      "name": "Keep active jobs only",
      "type": "n8n-nodes-base.filter",
      "position": [
        208,
        464
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "fd18f7b7-e45f-4cfd-b4ea-16a89af8fb78",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.active }}",
              "rightValue": "TRUE"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "f54d7c39-b63a-477f-9da1-a871d873b804",
      "name": "Flag jobs past their deadline",
      "type": "n8n-nodes-base.code",
      "position": [
        448,
        464
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set checker config').first().json;\nconst now = Date.now();\nconst reAlertMs = Number(cfg.re_alert_after_minutes) * 60000;\nconst out = [];\n\nfunction humanize(ms) {\n  const m = Math.floor(ms / 60000);\n  if (m < 60) return `${m}m`;\n  const h = Math.floor(m / 60);\n  if (h < 24) return `${h}h ${m % 60}m`;\n  return `${Math.floor(h / 24)}d ${h % 24}h`;\n}\n\nfor (const item of $input.all()) {\n  const j = item.json;\n  const everyMs = Number(j.expected_every_minutes) * 60000;\n  const graceMin = Number(j.grace_minutes) || Number(cfg.default_grace_minutes);\n  const deadlineMs = everyMs + graceMin * 60000;\n\n  const lastSeen  = j.last_heartbeat ? Date.parse(j.last_heartbeat) : null;\n  const lastAlert = j.last_alert_at  ? Date.parse(j.last_alert_at)  : null;\n\n  const overdue = lastSeen === null || (now - lastSeen) > deadlineMs;\n  if (!overdue) continue;\n\n  if (lastAlert && (now - lastAlert) < reAlertMs) continue;\n\n  out.push({\n    json: {\n      ...j,\n      alert_type: lastSeen === null ? 'NEVER_SEEN' : 'MISSED',\n      last_seen_human: lastSeen === null ? 'never' : humanize(now - lastSeen),\n      overdue_by_human: lastSeen === null ? 'n/a' : humanize(now - lastSeen - deadlineMs),\n      alert_channel: j.alert_channel || cfg.default_alert_channel,\n      checked_at: new Date(now).toISOString(),\n    },\n  });\n}\n\n  return out;"
      },
      "typeVersion": 2
    },
    {
      "id": "9ccb4d64-e3de-4004-a071-ae0390e11e25",
      "name": "Any jobs overdue?",
      "type": "n8n-nodes-base.if",
      "position": [
        608,
        464
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "a01eb885-86d2-425e-9fe9-7e81f7af36fc",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $input.all().length }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "800672e9-f596-481e-9257-a1a6f4707f8c",
      "name": "Nothing overdue",
      "type": "n8n-nodes-base.noOp",
      "position": [
        768,
        592
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "1d77a9dd-bb1d-4471-a00d-f596ca7ee5c6",
      "name": "Loop over overdue jobs",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1104,
        448
      ],
      "parameters": {
        "options": {},
        "batchSize": 10
      },
      "typeVersion": 3
    },
    {
      "id": "9dc1a218-b96d-49bc-993e-3bd69d40eee6",
      "name": "Post missed-run alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        1104,
        640
      ],
      "parameters": {
        "text": "={{ $json.alert_type === 'NEVER_SEEN' ? ':red_circle: *' + $json.job_name + '* has never checked in' : ':warning: *' + $json.job_name + '* missed its check-in' }} Job ID: `{{ $json.job_id }}` Last seen: {{ $json.last_seen_human }} Overdue by: {{ $json.overdue_by_human }} Expected every {{ $json.expected_every_minutes }} min",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#your-alert-channel"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "bd44a99b-7df6-4e56-b96d-e18b533b8f67",
      "name": "Mark job as alerted",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1328,
        640
      ],
      "parameters": {
        "columns": {
          "value": {
            "job_id": "={{ $('Loop over overdue jobs').item.json.job_id }}",
            "status": "={{ $('Loop over overdue jobs').item.json.alert_type }}",
            "last_alert_at": "={{ $now.toUTC().toISO() }}"
          },
          "schema": [
            {
              "id": "job_id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "job_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "job_name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "job_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "expected_every_minutes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "expected_every_minutes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "grace_minutes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "grace_minutes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "last_heartbeat",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "last_heartbeat",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "last_alert_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "last_alert_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "active",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "active",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "alert_channel",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "alert_channel",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "job_id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "5954ca11-5dc4-458c-9bd5-06c7b2135ad9",
      "name": "Was job previously alerting?",
      "type": "n8n-nodes-base.if",
      "position": [
        1200,
        -144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "4bf9602e-bd88-46b7-8b3f-5e4b3b643e20",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $('Look up job in registry').first().json.status }}",
              "rightValue": "OK"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "b6a06f8d-50f6-461c-8242-7c11c17097f4",
      "name": "Send recovery notice",
      "type": "n8n-nodes-base.slack",
      "position": [
        1392,
        -144
      ],
      "parameters": {
        "text": "=:white_check_mark: *{{ $('Look up job in registry').first().json.job_name }}* is checking in again",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#your-alert-channel"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5
    },
    {
      "id": "2e3510c3-1d1d-499b-808a-72e332a6516d",
      "name": "All alerts sent",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1328,
        448
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "489301c0-a903-4b27-9316-df5e1dfce2f4",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -992,
        96
      ],
      "parameters": {
        "color": "#EDF028",
        "width": 576,
        "height": 848,
        "content": "## Monitor job heartbeats and get alerted when a run goes missing\n\nMost automations tell you when something happened. This one tells you\nwhen something *didn't*. It is a dead man's switch: your jobs check in,\nand silence is the alarm.\n\n### How it works\nEach monitored job sends a POST to this workflow's webhook when it\nfinishes successfully. The workflow records the timestamp against that\njob in a Google Sheet.\n\nSeparately, a schedule runs every 15 minutes, reads the sheet, and\ncompares each job's last check-in against how often it is supposed to\nrun, plus a grace period. Anything past its deadline triggers a Slack\nalert. Jobs that have never checked in are flagged too. When a missing\njob checks back in, you get a recovery notice.\n\nRepeat alerts are suppressed for a configurable window, so a job that\nis down all weekend sends a handful of messages, not hundreds.\n\n### Setup\n1. Copy the sheet template and format all columns as plain text.\n2. Add your Google Sheets and Slack credentials.\n3. Open **Set receiver config** and **Set checker config** and fill in\n   your shared secret, sheet ID, and default alert channel.\n4. Add one row per job you want to watch.\n5. Activate, then have each job POST to the webhook on success.\n\n### Customization\nAdjust grace periods, alert channels, and the re-alert window per job\nin the sheet. Swap Slack for email, Telegram, or PagerDuty in one node."
      },
      "typeVersion": 1
    },
    {
      "id": "486b8b66-2a5e-4454-9ef2-7a9b09161987",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -624,
        -224
      ],
      "parameters": {
        "color": "#AA0E0E",
        "width": 208,
        "height": 272,
        "content": "## Set a shared secret\nThis webhook is public. Set `shared_secret` in **Set receiver config**\nand send it as an `x-heartbeat-secret` header, or anyone can spoof a\ncheck-in and silence your alerts."
      },
      "typeVersion": 1
    },
    {
      "id": "d572e677-368a-4260-a59d-bffc0a0622a6",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -384,
        -272
      ],
      "parameters": {
        "color": "#616161",
        "width": 768,
        "height": 384,
        "content": "## 1. Receive and authenticate\nJobs POST here when they finish successfully. The shared secret keeps\nstrangers out of your registry."
      },
      "typeVersion": 1
    },
    {
      "id": "9e530a64-6171-4c6f-94f3-5104fd3c555d",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        416,
        -272
      ],
      "parameters": {
        "color": "#616161",
        "width": 528,
        "height": 384,
        "content": "## 2. Find the job\nMatches the incoming job_id against the sheet. Unregistered jobs get a 404 rather than a silent failure."
      },
      "typeVersion": 1
    },
    {
      "id": "fd27d8de-60d7-4b95-9a76-57e83ae5138c",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1008,
        -272
      ],
      "parameters": {
        "color": "#616161",
        "width": 736,
        "height": 384,
        "content": "## 3. Record and confirm\nTimestamps the check-in. If the job was previously in alert, posts a\nrecovery notice before replying."
      },
      "typeVersion": 1
    },
    {
      "id": "44a95a8b-92d6-406c-90fe-0e38dcfb70ef",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -384,
        320
      ],
      "parameters": {
        "color": "#616161",
        "width": 752,
        "height": 336,
        "content": "## 4. Read the registry\nRuns on a schedule and loads every job. Rows marked inactive are\nskipped, so you can mute a job during maintenance."
      },
      "typeVersion": 1
    },
    {
      "id": "be5ad6ad-be51-4b12-ba4b-e7b224d2083b",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        416,
        320
      ],
      "parameters": {
        "color": "#616161",
        "width": 528,
        "height": 464,
        "content": "## 5. Find overdue jobs\nCompares each job's last check-in against its interval plus grace.\nJobs that never checked in are caught too. Recently alerted jobs stay quiet."
      },
      "typeVersion": 1
    },
    {
      "id": "35bcb8d6-e654-4cb9-b910-ea78a9a94027",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1024,
        320
      ],
      "parameters": {
        "color": "#616161",
        "width": 544,
        "height": 608,
        "content": "## 6. Alert and mark\nBatched Slack alerts, then writes back to the sheet so the same job\ndoes not alert repeatedly."
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "c7a223d0-c2fd-449a-9b63-9174c1eaf19c",
  "nodeGroups": [],
  "connections": {
    "Record heartbeat": {
      "main": [
        [
          {
            "node": "Was job previously alerting?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any jobs overdue?": {
      "main": [
        [
          {
            "node": "Loop over overdue jobs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Nothing overdue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is job registered?": {
      "main": [
        [
          {
            "node": "Record heartbeat",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return 404 unknown job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set checker config": {
      "main": [
        [
          {
            "node": "Get all monitored jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set receiver config": {
      "main": [
        [
          {
            "node": "Validate shared secret",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send recovery notice": {
      "main": [
        [
          {
            "node": "Return 200 OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep active jobs only": {
      "main": [
        [
          {
            "node": "Flag jobs past their deadline",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post missed-run alert": {
      "main": [
        [
          {
            "node": "Loop over overdue jobs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Mark job as alerted",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check every 15 minutes": {
      "main": [
        [
          {
            "node": "Set checker config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get all monitored jobs": {
      "main": [
        [
          {
            "node": "Keep active jobs only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop over overdue jobs": {
      "main": [
        [
          {
            "node": "All alerts sent",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Post missed-run alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Receive heartbeat ping": {
      "main": [
        [
          {
            "node": "Set receiver config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate shared secret": {
      "main": [
        [
          {
            "node": "Look up job in registry",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return 401 unauthorized",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Look up job in registry": {
      "main": [
        [
          {
            "node": "Is job registered?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Was job previously alerting?": {
      "main": [
        [
          {
            "node": "Send recovery notice",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return 200 OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flag jobs past their deadline": {
      "main": [
        [
          {
            "node": "Any jobs overdue?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}