{
  "name": "Send deduplicated workflow failure alerts with Error Trigger, Slack and email",
  "nodes": [
    {
      "parameters": {
        "content": "## Send deduplicated workflow failure alerts to Slack or email\n\nOne shared error workflow that every other workflow can point at, so a failure reaches you as a readable alert instead of sitting in the execution list.\n\n### Who's it for\nAnyone running more than a couple of scheduled workflows. The failure you never hear about is the expensive one.\n\n### How it works\n- **On Error Trigger** fires whenever a workflow that names this one as its error workflow fails.\n- **Normalize Error Data** reads both shapes n8n produces. A node failing mid-run gives you an execution block; the trigger itself failing does not. It also handles a missing execution URL, which only fills in when your instance saves executions.\n- The same node suppresses a repeat of the same failure inside a five-minute window, so one broken workflow cannot bury you in fifty alerts.\n- **Prepare Alert Message** builds one readable message with the workflow name, the node that failed, the error and a link to the run.\n- Send it to Slack, email, or both.\n\n### Setup\n1. Save this workflow under a name you will recognise in a dropdown, such as Error Handler.\n2. Open **Post Alert to Slack**, add your Slack credential and pick a channel. For email instead, enable **Email Alert Notification**, fill in both addresses and delete the Slack node.\n3. In every workflow you want covered, open Settings and set Error workflow to this one. It is per workflow. n8n has no instance-wide setting.\n\n### Requirements\nA Slack credential or SMTP credentials. No community nodes.\n\n### How to customize\nThe quiet window is QUIET_WINDOW_MS inside **Normalize Error Data**.\n\nFull walkthrough: https://www.theagentecosystem.com/blog/n8n-error-workflow-template",
        "height": 1014,
        "width": 760,
        "color": 1
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -1304
      ],
      "name": "Note: Overview"
    },
    {
      "parameters": {
        "content": "## 1. Catch and classify the failure\nReads both error shapes n8n produces, then decides whether this is new or a repeat inside the quiet window.",
        "height": 260,
        "width": 700,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        30
      ],
      "name": "Note: Catch and classify the failure"
    },
    {
      "parameters": {
        "content": "## 2. Format, or drop the repeat\nA new failure becomes one readable message. A repeat ends here instead of alerting again.",
        "height": 520,
        "width": 240,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        660,
        -70
      ],
      "name": "Note: Format, or drop the repeat"
    },
    {
      "parameters": {
        "content": "## 3. Deliver the alert\nSlack is enabled; the email step ships disabled. Keep one, or both.",
        "height": 460,
        "width": 240,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        920,
        -170
      ],
      "name": "Note: Deliver the alert"
    },
    {
      "parameters": {},
      "id": "a1f0c2d4-0000-4000-8000-000000000002",
      "name": "On Error Trigger",
      "type": "n8n-nodes-base.errorTrigger",
      "typeVersion": 1,
      "position": [
        0,
        180
      ]
    },
    {
      "parameters": {
        "jsCode": "// The Error Trigger emits a single item, in one of two shapes:\n//   a node failed mid-run     -> $json.execution.{ error, url, id, lastNodeExecuted }\n//   the trigger itself failed -> $json.trigger.error, and no execution block at all\n// Read both so the alert still lands in the second case.\nconst data = $input.first().json;\n\nconst execution = data.execution ?? {};\nconst trigger = data.trigger ?? {};\nconst workflow = data.workflow ?? {};\nconst error = execution.error ?? trigger.error ?? {};\n\nconst detail = {\n  workflowName: workflow.name ?? 'Unknown workflow',\n  workflowId: workflow.id ?? '',\n  message: error.message ?? 'No error message returned',\n  // Empty when the trigger itself failed, so the alert can reword rather than\n  // print a placeholder where a node name belongs.\n  lastNode: execution.lastNodeExecuted ?? '',\n  executionId: execution.id ?? '',\n  // execution.url and execution.id only populate when the instance saves executions.\n  executionUrl: execution.url ?? '',\n  failedAt: new Date().toISOString(),\n};\n\n// Dedupe. Suppress a repeat of the same failure inside the quiet window so a single\n// broken workflow cannot send fifty alerts. State survives between runs in static data.\nconst QUIET_WINDOW_MS = 5 * 60 * 1000;\nconst key = detail.workflowName + '::' + detail.message;\n\nconst state = $getWorkflowStaticData('global');\nstate.lastAlerts = state.lastAlerts ?? {};\n\nconst previous = state.lastAlerts[key] ?? 0;\nconst now = Date.now();\nconst suppressed = now - previous < QUIET_WINDOW_MS;\n\nif (!suppressed) {\n  state.lastAlerts[key] = now;\n}\n\nreturn [{ json: { ...detail, suppressed } }];"
      },
      "id": "a1f0c2d4-0000-4000-8000-000000000003",
      "name": "Normalize Error Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        180
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "b2e1d3c5-0000-4000-8000-000000000001",
              "leftValue": "={{ $json.suppressed }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a1f0c2d4-0000-4000-8000-000000000004",
      "name": "If Alert Not Suppressed",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        460,
        180
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c3d2e4f6-0000-4000-8000-000000000001",
              "name": "alertText",
              "value": "=Workflow *{{ $json.workflowName }}* {{ $json.lastNode ? 'failed at node *' + $json.lastNode + '*' : 'failed before any node ran — the trigger itself errored' }}\n\n{{ $json.message }}\n\n{{ $json.executionUrl || 'No execution URL — this instance may not be saving executions.' }}",
              "type": "string"
            },
            {
              "id": "c3d2e4f6-0000-4000-8000-000000000002",
              "name": "alertSubject",
              "value": "=[n8n] {{ $json.workflowName }} failed",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "a1f0c2d4-0000-4000-8000-000000000005",
      "name": "Prepare Alert Message",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        700,
        80
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "",
          "mode": "name"
        },
        "text": "={{ $json.alertText }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "a1f0c2d4-0000-4000-8000-000000000006",
      "name": "Post Alert to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        960,
        -20
      ],
      "credentials": {
        "slackApi": {
          "name": "Slack account"
        }
      }
    },
    {
      "parameters": {
        "fromEmail": "n8n-alerts@example.com",
        "toEmail": "you@example.com",
        "subject": "={{ $json.alertSubject }}",
        "emailFormat": "text",
        "text": "={{ $json.alertText }}",
        "options": {}
      },
      "id": "a1f0c2d4-0000-4000-8000-000000000007",
      "name": "Email Alert Notification",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        960,
        180
      ],
      "disabled": true,
      "credentials": {
        "smtp": {
          "name": "SMTP account"
        }
      }
    },
    {
      "parameters": {},
      "id": "a1f0c2d4-0000-4000-8000-000000000008",
      "name": "Suppress Duplicate Alert",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        700,
        340
      ]
    }
  ],
  "connections": {
    "On Error Trigger": {
      "main": [
        [
          {
            "node": "Normalize Error Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Error Data": {
      "main": [
        [
          {
            "node": "If Alert Not Suppressed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Alert Not Suppressed": {
      "main": [
        [
          {
            "node": "Prepare Alert Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Suppress Duplicate Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Alert Message": {
      "main": [
        [
          {
            "node": "Post Alert to Slack",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Alert Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false,
  "tags": []
}
