{
  "name": "Alert when a scheduled workflow fails, skips a run or processes zero items with Vivere",
  "nodes": [
    {
      "parameters": {
        "content": "## Know when a scheduled workflow fails, skips a run or handles nothing\n\n**Who it's for:** anyone with a scheduled workflow (a nightly sync, a report, a backup, a scraper) whose silence would go unnoticed.\n\n**What it does:** reports every run to a [Vivere](https://vivere.dev/docs/n8n) heartbeat monitor. Vivere expects a ping on your schedule and alerts you by email, Slack, Discord, Telegram, ntfy or webhook when a run fails, handles zero items, or never arrives at all.\n\n### How it works\n1. **Schedule Trigger** starts the run and **Ping start** tells Vivere it began, so each run's duration is recorded.\n2. The **example step** fetches the latest n8n releases from GitHub. Replace it with your own steps.\n3. **Check the run** sorts the result into one of three outcomes, and each outcome sends exactly one ping: success with `?items=<count>`, fail for zero items, or fail with the error message.\n4. If the workflow never runs at all, no ping arrives and Vivere alerts when the deadline passes.\n\n### Setup (about 5 minutes)\n1. Sign up at [vivere.dev](https://vivere.dev) (the free plan covers 25 monitors, with email and webhook alerts) and add a **Heartbeat** monitor with the same schedule as the trigger: here, period `1 day` and a grace of, say, `30 minutes`.\n2. Copy its ping URL into the **Config** node.\n3. Replace the example step with your own steps, connect the last one to **Check the run**, and copy the example's two settings onto it (see the note above it).\n4. Activate the workflow.\n\n### Customize\n- Change the schedule, and give the monitor the same one (a cron expression works too).\n- Treat small runs as failures: in **Check the run**, change `count > 0` to, say, `count >= 100`.\n- The ping bodies are kept as each run's log in Vivere, so put whatever helps you debug there.",
        "height": 820,
        "width": 560
      },
      "id": "e5d24aea-6b71-53d3-9522-c07b0f4d222e",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -720,
        -240
      ]
    },
    {
      "parameters": {
        "content": "## 1. Start the run\n**Config** holds the monitor's ping URL, the only thing to fill in. Every ping node reads it from there.\n\n**Ping start** records the start time, so Vivere shows how long each run took.",
        "height": 460,
        "width": 660,
        "color": 7
      },
      "id": "dbd84c78-5be2-5051-95d1-69f0a9e5c65b",
      "name": "Note: start the run",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -120,
        120
      ]
    },
    {
      "parameters": {
        "content": "## Why a heartbeat, and not only the Error Trigger\nn8n's **Error Trigger** fires when an execution fails, so an execution has to exist first. A schedule that never fires creates no execution and no error:\n- the instance was down or restarting at the scheduled minute\n- the workflow was deactivated by an import, a restore or repeated trigger errors\n- the cron or timezone is not the one you think\n\nA heartbeat works the other way round: Vivere expects a ping and alerts when it does **not** arrive. That catches the run that never started, and a run that crashed in an earlier step before it could report.",
        "height": 340,
        "width": 660,
        "color": 6
      },
      "id": "95080a0e-e19e-5a8e-ae9d-ef85e3764340",
      "name": "Note: why a heartbeat",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -120,
        -240
      ]
    },
    {
      "parameters": {
        "content": "## 2. Your workflow goes here\nThe example fetches the 10 latest n8n releases from GitHub's public API, with no credentials. **Replace it with your own steps** and connect the last one to **Check the run**.\n\nGive that last step the example's two settings (node **Settings** tab):\n- **Always Output Data: on.** A node that returns zero items stops its branch: n8n skips everything after it and still marks the execution *Success*, so nothing would report the empty run. With this on, an empty result reaches **Check the run** as one empty item.\n- **On Error: Continue.** A failure arrives as an item with an `error` field, instead of ending the run before it can report.\n\nDon't use *Continue (using error output)* here: with Always Output Data on, n8n sends an empty item down **both** outputs whenever one is empty, so one run would send two pings.",
        "height": 820,
        "width": 420,
        "color": 5
      },
      "id": "c0688477-3e2f-5b72-80f3-f3cc4b80ed06",
      "name": "Note: your workflow",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        560,
        -240
      ]
    },
    {
      "parameters": {
        "content": "## 3. Check and report: one ping per run\n**Check the run** looks at what the last step returned:\n- **Items handled** → **Ping success** with `?items=<count>`. Vivere shows the count for each run.\n- **No items** → **Ping fail (0 items)**. The run \"succeeded\" but did nothing, which usually means a broken source, an expired token or a filter gone wrong.\n- **Step failed** → **Ping fail (step error)** with the error message as the run log. The monitor goes down at once instead of waiting for the deadline. **Stop and Error** then fails the execution, so it still shows red in n8n and still runs your Error Workflow.\n\nEach ping has a 10 s timeout, 3 tries, **Execute Once** and **On Error: Continue**, so a hiccup on the monitoring side never breaks your workflow.",
        "height": 1000,
        "width": 960,
        "color": 7
      },
      "id": "099d7f5f-948e-57af-b456-a142fc02f0bb",
      "name": "Note: check and report",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1000,
        -240
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 6
            }
          ]
        }
      },
      "id": "1561c948-30f3-50fa-bf7d-e52440040809",
      "name": "Every day at 06:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -60,
        360
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "df8e569a-f90c-51e2-afdf-b23e5ef93f1f",
              "name": "pingUrl",
              "value": "https://vivere.dev/p/YOUR-MONITOR-ID",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "8b29b231-ce59-5a99-8f1f-ff21b7ac4c08",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        160,
        360
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').first().json.pingUrl }}/start",
        "options": {
          "timeout": 10000
        }
      },
      "id": "d4faa0a7-2fae-5f0c-96af-76834fc02f98",
      "name": "Ping start",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        380,
        360
      ],
      "executeOnce": true,
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "https://api.github.com/repos/n8n-io/n8n/releases",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "per_page",
              "value": "10"
            }
          ]
        },
        "options": {
          "timeout": 30000
        }
      },
      "id": "28638632-3180-50a2-95dc-d0a15909db87",
      "name": "Get n8n releases (example step)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        720,
        360
      ],
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Sort the last step's output into one outcome, so exactly one ping is sent.\n// With Always Output Data and On Error: Continue, it receives one of:\n//   the items | one empty item (nothing returned) | one item with only `error`\nconst items = $input.all().map(item => item.json);\nconst errorItem = items.find(j => Object.keys(j).length === 1 && j.error !== undefined);\nif (errorItem) {\n  const e = errorItem.error;\n  const message = typeof e === 'string' ? e : (e?.message || JSON.stringify(e));\n  return [{ json: { outcome: 'failed', count: 0, message: message || 'The step failed without an error message' } }];\n}\n\nconst count = items.filter(j => Object.keys(j).length > 0).length;\nreturn [{ json: { outcome: count > 0 ? 'items' : 'empty', count } }];"
      },
      "id": "40fea46e-041d-5f29-b2b4-272c794b3e27",
      "name": "Check the run",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1060,
        360
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "4087ce19-c927-5b13-8de9-96d4e8c04bb3",
                    "leftValue": "={{ $json.outcome }}",
                    "rightValue": "items",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Items handled"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "0b8f15f9-b301-5a68-a7b9-8ad6644ac160",
                    "leftValue": "={{ $json.outcome }}",
                    "rightValue": "empty",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "No items"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "2ad6d360-8fc9-5c33-9a7d-caed2dee1477",
                    "leftValue": "={{ $json.outcome }}",
                    "rightValue": "failed",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Step failed"
            }
          ]
        },
        "options": {}
      },
      "id": "c211fda7-b4ec-5d9f-b922-664f14163df2",
      "name": "Route by outcome",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1280,
        360
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').first().json.pingUrl }}",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "items",
              "value": "={{ $json.count }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "text/plain",
        "body": "=Run finished: {{ $json.count }} items handled.",
        "options": {
          "timeout": 10000
        }
      },
      "id": "f697cd59-9b30-5663-933a-d6bd22de3b5a",
      "name": "Ping success",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1540,
        160
      ],
      "executeOnce": true,
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').first().json.pingUrl }}/fail",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "items",
              "value": "0"
            }
          ]
        },
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "text/plain",
        "body": "=Run finished with 0 items: the last step returned nothing.",
        "options": {
          "timeout": 10000
        }
      },
      "id": "49465110-8e43-5e04-a0f6-a7101b6eaf41",
      "name": "Ping fail (0 items)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1540,
        360
      ],
      "executeOnce": true,
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').first().json.pingUrl }}/fail",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "text/plain",
        "body": "={{ $json.message }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "3b0af494-6d2b-5ab2-8885-5542d544fa60",
      "name": "Ping fail (step error)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1540,
        560
      ],
      "executeOnce": true,
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "errorMessage": "={{ $('Check the run').first().json.message }}"
      },
      "id": "dfab2340-7b9d-55fc-9119-bfbbabc9c6c2",
      "name": "Stop and Error",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1760,
        560
      ]
    }
  ],
  "connections": {
    "Every day at 06:00": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Ping start",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ping start": {
      "main": [
        [
          {
            "node": "Get n8n releases (example step)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get n8n releases (example step)": {
      "main": [
        [
          {
            "node": "Check the run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check the run": {
      "main": [
        [
          {
            "node": "Route by outcome",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by outcome": {
      "main": [
        [
          {
            "node": "Ping success",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ping fail (0 items)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ping fail (step error)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ping fail (step error)": {
      "main": [
        [
          {
            "node": "Stop and Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
