{
  "name": "Silent failure check: green runs that did nothing",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      },
      "id": "c2b0d1e2-0002-4a10-9c01-000000000001",
      "name": "Every Hour",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -540,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Configure the check here. One entry per workflow you actually care about.\n//\n//   workflowId    the id in the /workflow/<id> URL\n//   label         what to call it in the alert\n//   minRuns       how many successful runs you expect in the window below\n//   windowHours   how far back to look\n//   floorMs       a run faster than this almost certainly processed nothing\n//\n// floorMs is the whole trick. A workflow that fetches rows, loops and writes\n// cannot finish in 400ms. When it does, an upstream filter returned zero items\n// and every node after it was skipped, which n8n records as a success.\n\nreturn [{\n  json: {\n    n8nBaseUrl: 'https://n8n.example.com',\n    checks: [\n      { workflowId: 'REPLACE_ME', label: 'Lead routing', minRuns: 4, windowHours: 1, floorMs: 800 },\n      { workflowId: 'REPLACE_ME_TOO', label: 'Nightly invoice sync', minRuns: 1, windowHours: 24, floorMs: 2000 }\n    ]\n  }\n}];"
      },
      "id": "c2b0d1e2-0002-4a10-9c01-000000000002",
      "name": "Checks To Run",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -320,
        0
      ]
    },
    {
      "parameters": {
        "fieldToSplitOut": "checks",
        "include": "selectedOtherFields",
        "fieldsToInclude": "n8nBaseUrl",
        "options": {}
      },
      "id": "c2b0d1e2-0002-4a10-9c01-000000000003",
      "name": "One Item Per Check",
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        -100,
        0
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.n8nBaseUrl }}/api/v1/executions",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "workflowId",
              "value": "={{ $json.workflowId }}"
            },
            {
              "name": "status",
              "value": "success"
            },
            {
              "name": "limit",
              "value": "100"
            }
          ]
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-N8N-API-KEY",
              "value": "={{ $env.N8N_API_KEY }}"
            },
            {
              "name": "accept",
              "value": "application/json"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true
            }
          }
        }
      },
      "id": "c2b0d1e2-0002-4a10-9c01-000000000004",
      "name": "List Successful Runs",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        120,
        0
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Emit ONE item per problem. When everything is healthy this returns nothing,\n// the Slack node never runs, and you get no message. Silence means silence.\n\nconst problems = [];\n\nfor (let i = 0; i < items.length; i++) {\n  const res = items[i].json;\n  const check = $('One Item Per Check').all()[i].json;\n  const label = check.label || check.workflowId;\n\n  if (res.statusCode && res.statusCode >= 400) {\n    problems.push({\n      json: {\n        label,\n        kind: 'api-unreachable',\n        detail: 'The n8n API answered ' + res.statusCode + '. Check N8N_API_KEY and that the public API is enabled.'\n      }\n    });\n    continue;\n  }\n\n  const body = res.body || res;\n  const runs = Array.isArray(body.data) ? body.data : [];\n  const cutoff = Date.now() - (check.windowHours * 60 * 60 * 1000);\n\n  const recent = runs.filter(r => new Date(r.startedAt).getTime() >= cutoff);\n\n  // Case 1: the trigger itself stopped firing. No executions at all is the\n  // failure people notice last, because there is no red anywhere to see.\n  if (recent.length < check.minRuns) {\n    problems.push({\n      json: {\n        label,\n        kind: 'not-running',\n        detail: 'Expected at least ' + check.minRuns + ' successful run(s) in the last ' +\n                check.windowHours + 'h, found ' + recent.length + '. The trigger may be off, ' +\n                'the workflow deactivated, or the schedule never fired.'\n      }\n    });\n  }\n\n  // Case 2: it ran, it went green, and it finished too fast to have done work.\n  const durations = recent\n    .filter(r => r.stoppedAt)\n    .map(r => ({ id: r.id, ms: new Date(r.stoppedAt) - new Date(r.startedAt) }));\n\n  const tooFast = durations.filter(d => d.ms < check.floorMs);\n\n  if (tooFast.length > 0) {\n    const sorted = durations.map(d => d.ms).sort((a, b) => a - b);\n    const median = sorted.length ? sorted[Math.floor(sorted.length / 2)] : 0;\n    problems.push({\n      json: {\n        label,\n        kind: 'ran-but-empty',\n        detail: tooFast.length + ' of ' + recent.length + ' successful runs finished under ' +\n                check.floorMs + 'ms (median ' + median + 'ms). Green, and almost certainly ' +\n                'processing zero items. Executions: ' + tooFast.slice(0, 5).map(d => d.id).join(', ')\n      }\n    });\n  }\n}\n\nreturn problems;"
      },
      "id": "c2b0d1e2-0002-4a10-9c01-000000000005",
      "name": "Find Silent Failures",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        340,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://hooks.slack.com/services/REPLACE/WITH/YOUR_WEBHOOK",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: ':mag: Silent failure: ' + $json.label, blocks: [ { type: 'section', text: { type: 'mrkdwn', text: '*' + $json.label + '* (' + $json.kind + ')\\n' + $json.detail } } ] }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "id": "c2b0d1e2-0002-4a10-9c01-000000000006",
      "name": "Alert Slack",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        560,
        0
      ],
      "onError": "continueRegularOutput"
    }
  ],
  "connections": {
    "Every Hour": {
      "main": [
        [
          {
            "node": "Checks To Run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Checks To Run": {
      "main": [
        [
          {
            "node": "One Item Per Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One Item Per Check": {
      "main": [
        [
          {
            "node": "List Successful Runs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Successful Runs": {
      "main": [
        [
          {
            "node": "Find Silent Failures",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Silent Failures": {
      "main": [
        [
          {
            "node": "Alert Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {
    "instanceId": "shared-template"
  }
}