{
  "name": "Monitor a self-hosted n8n instance for downtime and failed executions with Vivere",
  "nodes": [
    {
      "parameters": {
        "content": "## A watchdog for your self-hosted n8n\n\n**Who it's for:** anyone running n8n themselves (Docker, a VPS, Kubernetes) who wants to know when the instance stops working, not just when one workflow throws.\n\n**What it does:** every 5 minutes it asks your instance's API for failed executions and pings a [Vivere](https://vivere.dev/docs/n8n) heartbeat monitor: a success ping when nothing failed, a fail ping listing the failures when something did. Because the pings come from inside the instance, they stop when it stops, and Vivere alerts you when they don't arrive.\n\nThat covers what n8n can't report about itself:\n- the host or container is down, or didn't come back after a restart\n- an update left the instance up but its schedules no longer firing\n- the database or queue is stuck, so nothing runs\n\n### How it works\n1. **Every 5 minutes** starts a check; **Config** holds the ping URL and the look-back window.\n2. **Get failed executions** reads up to 100 recent executions with status *error* through the n8n API.\n3. **Check executions** keeps those that ended inside the look-back window, leaving out this workflow's own.\n4. None → **Ping success**. Some → **Get workflow names**, **Build failure summary**, then **Ping fail** with the list as the run log. API unreachable → **Ping fail (API error)**.\n\n### Setup (about 5 minutes)\n1. In n8n, go to **Settings → n8n API**, create an API key, and add an **n8n API** credential with it. Its base URL is your instance's URL plus `/api/v1`. Select it in both n8n nodes.\n2. Sign up at [vivere.dev](https://vivere.dev) (the free plan covers 25 monitors, with email and webhook alerts and a shortest period of 5 minutes) and add a **Heartbeat** monitor with period `5 minutes` and grace `5 minutes`.\n3. Copy its ping URL into **Config** and activate the workflow.\n\n### Customize\n- Check less often: change the trigger, `lookbackMinutes` and the monitor's period together.\n- Ignore noisy workflows: filter them out by id in **Check executions**.",
        "height": 900,
        "width": 560
      },
      "id": "baff15f7-a383-5d2b-a529-43b68126d91a",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -720,
        -240
      ]
    },
    {
      "parameters": {
        "content": "## 1. Check every 5 minutes\n**Config:**\n- `pingUrl`: the monitor's ping URL\n- `lookbackMinutes`: how far back to look for failures. Keep it equal to the trigger's interval, so each failure is reported once.\n\nGive the monitor the same 5-minute period and a few minutes' grace. If the instance goes down, the pings stop and Vivere alerts you once period plus grace has passed.",
        "height": 660,
        "width": 480,
        "color": 7
      },
      "id": "f5e4e72a-9902-5887-88b3-2046b9b6dad9",
      "name": "Note: check",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -120,
        -240
      ]
    },
    {
      "parameters": {
        "content": "## 2. Read failed executions\nUses an **n8n API** credential (**Settings → n8n API**), with base URL `https://your-n8n.example.com/api/v1`.\n\nTwo settings on **Get failed executions** matter:\n- **Always Output Data: on.** With no failed executions the node returns nothing, and n8n would skip everything after it, success ping included.\n- **On Error: Continue.** If the API can't be reached or the key is wrong, the error goes on to **Check executions**, which reports it.",
        "height": 660,
        "width": 440,
        "color": 5
      },
      "id": "7a7f756c-cb10-517b-b7e0-454cc142eac3",
      "name": "Note: read",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        380,
        -240
      ]
    },
    {
      "parameters": {
        "content": "## 3. Report to Vivere: one ping per check\n**Route by outcome:**\n- **No failures** → **Ping success**. The monitor stays up and its deadline moves on.\n- **Failures** → workflow names are looked up (only now, to keep the 5-minute check light), and **Ping fail** sends the list. The monitor goes down at once with the list as the run log, and comes back up on the next clean check.\n- **API error** → **Ping fail (API error)** with the reason, since a watchdog that can't read the API is not watching.\n\nEach ping has a 10 s timeout, 3 tries and **On Error: Continue**.",
        "height": 800,
        "width": 960,
        "color": 7
      },
      "id": "37a54534-4bea-5e1d-ab98-59ded4f01222",
      "name": "Note: report",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        840,
        -240
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      },
      "id": "10f3e501-2882-5b0b-ba62-4eb9338a6247",
      "name": "Every 5 minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -60,
        200
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a1934ab7-5f9f-512b-99ff-62125448a42f",
              "name": "pingUrl",
              "value": "https://vivere.dev/p/YOUR-MONITOR-ID",
              "type": "string"
            },
            {
              "id": "7adb948c-17c6-51d5-b0f6-c8ea599336a0",
              "name": "lookbackMinutes",
              "value": 5,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "3f372afc-98f6-528b-be8c-f1c0c0a2fbb3",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        160,
        200
      ]
    },
    {
      "parameters": {
        "resource": "execution",
        "operation": "getAll",
        "limit": 100,
        "filters": {
          "status": "error"
        },
        "options": {},
        "requestOptions": {}
      },
      "id": "28900694-163b-5980-b55c-58a2e11d788b",
      "name": "Get failed executions",
      "type": "n8n-nodes-base.n8n",
      "typeVersion": 1,
      "position": [
        460,
        200
      ],
      "alwaysOutputData": true,
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Keep failed executions that ended inside the look-back window, except this\n// workflow's own, and sort the check into one outcome so exactly one ping is sent.\n// Always Output Data adds one empty item when there are none; On Error: Continue\n// passes an API failure on as one item with only an `error` field.\nconst cfg = $('Config').first().json;\nconst lookbackMinutes = Number(cfg.lookbackMinutes) || 5;\nconst since = Date.now() - lookbackMinutes * 60 * 1000;\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: 'apiError', lookbackMinutes, message: message || 'unknown error' } }];\n}\n\nconst failures = items\n  .filter(e => e.id !== undefined && e.workflowId !== $workflow.id)\n  .filter(e => new Date(e.stoppedAt || e.startedAt).getTime() >= since)\n  .map(e => ({\n    executionId: e.id,\n    workflowId: e.workflowId,\n    mode: e.mode,\n    failedAt: e.stoppedAt || e.startedAt,\n  }));\n\nreturn [{ json: {\n  outcome: failures.length ? 'failures' : 'none',\n  lookbackMinutes,\n  count: failures.length,\n  failures,\n} }];"
      },
      "id": "e3965f89-824e-53fe-a62d-ae81a8659c2b",
      "name": "Check executions",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        200
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "1cc32aa8-8c85-5803-85fb-de835e2dbdaa",
                    "leftValue": "={{ $json.outcome }}",
                    "rightValue": "none",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "No failures"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "68c064f8-6d4b-52b7-9433-7bbdeafe3870",
                    "leftValue": "={{ $json.outcome }}",
                    "rightValue": "failures",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Failures"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "eb16d645-a01e-51ab-a649-fbc0a1684b9b",
                    "leftValue": "={{ $json.outcome }}",
                    "rightValue": "apiError",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "API error"
            }
          ]
        },
        "options": {}
      },
      "id": "0126250f-378f-50ba-80c7-cf99c4fa7a57",
      "name": "Route by outcome",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        920,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').first().json.pingUrl }}",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "text/plain",
        "body": "=0 failed executions in the last {{ $json.lookbackMinutes }} min.",
        "options": {
          "timeout": 10000
        }
      },
      "id": "cef2ea56-6a3e-5c33-8827-da7ea5e8dce3",
      "name": "Ping success",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1180,
        0
      ],
      "executeOnce": true,
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "getAll",
        "returnAll": true,
        "filters": {
          "excludePinnedData": true
        },
        "requestOptions": {}
      },
      "id": "ad25b88b-c70b-55be-a5e2-70edaa9d028f",
      "name": "Get workflow names",
      "type": "n8n-nodes-base.n8n",
      "typeVersion": 1,
      "position": [
        1180,
        200
      ],
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Turn the failures into a short, readable run log for Vivere.\nconst { count, lookbackMinutes, failures } = $('Check executions').first().json;\nconst names = {};\nfor (const item of $input.all()) {\n  if (item.json.id) names[item.json.id] = item.json.name;\n}\n\nconst lines = failures.map(f => {\n  const name = names[f.workflowId] ? `${names[f.workflowId]} (${f.workflowId})` : `workflow ${f.workflowId}`;\n  return `- ${name}: execution ${f.executionId}, ${f.mode}, failed ${f.failedAt}`;\n});\nconst more = count >= 100 ? ' (or more)' : '';\nconst summary = `${count}${more} failed execution${count === 1 ? '' : 's'} in the last ${lookbackMinutes} min:\\n` + lines.join('\\n');\n\nreturn [{ json: { count, summary } }];"
      },
      "id": "127a4e21-7a06-58c0-9f29-c526ef6d67a0",
      "name": "Build failure summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1400,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $('Config').first().json.pingUrl }}/fail",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "text/plain",
        "body": "={{ $json.summary }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "f10c6ae2-f3e9-520c-bc01-ed368b031a15",
      "name": "Ping fail",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1620,
        200
      ],
      "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": "=Watchdog could not read executions from the n8n API: {{ $json.message }}",
        "options": {
          "timeout": 10000
        }
      },
      "id": "bbb1db5b-6754-558f-aa89-217fdf6192f5",
      "name": "Ping fail (API error)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1180,
        400
      ],
      "executeOnce": true,
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000,
      "onError": "continueRegularOutput"
    }
  ],
  "connections": {
    "Every 5 minutes": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Get failed executions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get failed executions": {
      "main": [
        [
          {
            "node": "Check executions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check executions": {
      "main": [
        [
          {
            "node": "Route by outcome",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by outcome": {
      "main": [
        [
          {
            "node": "Ping success",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Get workflow names",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Ping fail (API error)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get workflow names": {
      "main": [
        [
          {
            "node": "Build failure summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build failure summary": {
      "main": [
        [
          {
            "node": "Ping fail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}
