{
  "name": "Monitor Twilio debugger alerts and post a grouped Slack digest",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 60
            }
          ]
        }
      },
      "id": "a95d83e1-9c8d-49bf-96a7-65e617c3fdfa",
      "name": "Check Twilio on a Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        -64,
        752
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "lookback-minutes",
              "name": "lookbackMinutes",
              "value": 60,
              "type": "number"
            },
            {
              "id": "start-date",
              "name": "startDate",
              "value": "={{ $now.minus({ minutes: 60 }).toUTC().toISO() }}",
              "type": "string"
            },
            {
              "id": "page-size",
              "name": "pageSize",
              "value": 100,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "983c92b5-a896-429d-88a2-d9e983c50789",
      "name": "Compute Alert Window",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        176,
        752
      ]
    },
    {
      "parameters": {
        "url": "https://monitor.twilio.com/v1/Alerts",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "twilioApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "StartDate",
              "value": "={{ $json.startDate }}"
            },
            {
              "name": "PageSize",
              "value": "={{ $json.pageSize }}"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "ca09b664-59b4-4f5e-8e64-e4b09c98a47d",
      "name": "Fetch Twilio Debugger Alerts",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        560,
        752
      ],
      "credentials": {
        "twilioApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "alerts-not-empty",
              "leftValue": "={{ $json.alerts }}",
              "operator": {
                "type": "array",
                "operation": "notEmpty"
              },
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "8430d40a-82dd-4c7c-a4b4-41b16f3c7eaa",
      "name": "Any Alerts Returned?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        800,
        752
      ]
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst alerts = Array.isArray(response.alerts) ? response.alerts : [];\n\nconst groups = {};\nconst rows = [];\n\nfor (const alert of alerts) {\n  const errorCode = String(alert.error_code === undefined || alert.error_code === null ? 'unknown' : alert.error_code);\n  const logLevel = String(alert.log_level || 'unknown');\n  const key = errorCode + '|' + logLevel;\n\n  rows.push({\n    timestamp: alert.date_generated || '',\n    errorCode: errorCode,\n    logLevel: logLevel,\n    resourceSid: alert.resource_sid || '',\n    requestUrl: alert.request_url || '',\n    alertSid: alert.sid || ''\n  });\n\n  if (!groups[key]) {\n    groups[key] = { errorCode: errorCode, logLevel: logLevel, count: 0, sampleMessage: '', moreInfo: alert.more_info || '' };\n  }\n  groups[key].count = groups[key].count + 1;\n  if (!groups[key].sampleMessage) {\n    groups[key].sampleMessage = String(alert.alert_text || '').slice(0, 140);\n  }\n}\n\nconst grouped = Object.values(groups).sort(function (a, b) { return b.count - a.count; });\n\nconst severity = {};\nfor (const group of grouped) {\n  severity[group.logLevel] = (severity[group.logLevel] || 0) + group.count;\n}\n\nconst severityLine = Object.keys(severity).sort().map(function (level) {\n  return level + ': ' + severity[level];\n}).join(', ');\n\nconst topLines = grouped.slice(0, 5).map(function (group) {\n  return '- ' + group.errorCode + ' (' + group.logLevel + ') x' + group.count + ' ' + group.sampleMessage;\n}).join('\\n');\n\nconst digest = '*Twilio debugger digest*\\n' +\n  'Total alerts: ' + alerts.length + '\\n' +\n  'Severity: ' + (severityLine || 'none') + '\\n' +\n  'Distinct groups: ' + grouped.length + '\\n\\n' +\n  'Top error codes:\\n' + (topLines || '- none');\n\nreturn rows.map(function (row) {\n  return {\n    json: {\n      timestamp: row.timestamp,\n      errorCode: row.errorCode,\n      logLevel: row.logLevel,\n      resourceSid: row.resourceSid,\n      requestUrl: row.requestUrl,\n      alertSid: row.alertSid,\n      totalAlerts: alerts.length,\n      distinctGroups: grouped.length,\n      severityBreakdown: severityLine,\n      digest: digest\n    }\n  };\n});"
      },
      "id": "91defb78-3c74-45b1-b8e4-5088d784309e",
      "name": "Group Alerts by Code and Severity",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1248,
        736
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "REPLACE_WITH_YOUR_SPREADSHEET"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "REPLACE_WITH_YOUR_SHEET_TAB"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "timestamp": "={{ $json.timestamp }}",
            "error_code": "={{ $json.errorCode }}",
            "log_level": "={{ $json.logLevel }}",
            "resource_sid": "={{ $json.resourceSid }}",
            "request_url": "={{ $json.requestUrl }}",
            "alert_sid": "={{ $json.alertSid }}"
          },
          "schema": [
            {
              "id": "timestamp",
              "displayName": "timestamp",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "error_code",
              "displayName": "error_code",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "log_level",
              "displayName": "log_level",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "resource_sid",
              "displayName": "resource_sid",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "request_url",
              "displayName": "request_url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "alert_sid",
              "displayName": "alert_sid",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {
          "cellFormat": "USER_ENTERED"
        }
      },
      "id": "1a032299-a2e1-4b0e-9d34-d87a1143ed08",
      "name": "Append Alert Rows to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        1808,
        736
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "REPLACE_WITH_YOUR_SLACK_CHANNEL"
        },
        "text": "={{ $json.digest }}",
        "otherOptions": {
          "includeLinkToWorkflow": false,
          "mrkdwn": true
        }
      },
      "id": "b56e0617-6edb-47ff-964d-b8a4cec68746",
      "name": "Post Grouped Digest to Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.5,
      "position": [
        1808,
        944
      ],
      "executeOnce": true,
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "content": "## Monitor Twilio debugger alerts and post a grouped Slack digest\n\n### How it works\n\n1. `Check Twilio on a Schedule` fires on the interval you set, hourly by default.\n2. `Compute Alert Window` builds an ISO-8601 start date of now minus the lookback minutes, plus a page size.\n3. `Fetch Twilio Debugger Alerts` GETs the Twilio Monitor Alerts API over HTTP Basic Auth, filtered to that window.\n4. `Any Alerts Returned?` ends the run when the alerts array is empty, so a quiet hour stays silent.\n5. `Group Alerts by Code and Severity` flattens each alert into a row, then collapses them by error code and log level.\n6. `Append Alert Rows to Sheet` writes one row per alert, and `Post Grouped Digest to Slack` posts one message per run.\n\n### Setup steps\n\n- [ ] Add an HTTP Basic Auth credential with your Twilio Account SID as the user and your Auth Token as the password, then assign it to `Fetch Twilio Debugger Alerts`.\n- [ ] Add a Google Sheets credential and a Slack credential for the two output nodes.\n- [ ] Pick the spreadsheet and tab, and give the tab this header row: timestamp, error_code, log_level, resource_sid, request_url, alert_sid.\n- [ ] Pick the channel in `Post Grouped Digest to Slack`.\n- [ ] Set the interval in `Check Twilio on a Schedule`, then match `lookbackMinutes` in `Compute Alert Window` so the window covers the gap between runs.\n\n### Customization\n\nRaise the page size for a busy account, filter the Code node to one log level if warnings are noise, or drop the Sheets branch and keep the Slack digest on its own.",
        "height": 704,
        "width": 992
      },
      "id": "c5341e41-9192-4f7b-bb5c-832c2f837360",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -160,
        -192
      ]
    },
    {
      "parameters": {
        "content": "## Required credential and read-only by design\n\nThis calls Twilio's Monitor Alerts API over raw HTTP, so it needs an HTTP Basic Auth credential built from your Account SID and Auth Token. The native Twilio node cannot supply it. The workflow only reads. It never sends a message.",
        "height": 240,
        "width": 448,
        "color": 3
      },
      "id": "9b88efc0-1018-4ba5-8b99-7974182d2e49",
      "name": "Credential and Safety Warning",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        896,
        -192
      ]
    },
    {
      "parameters": {
        "content": "## Set the alert window\n\nA schedule trigger fires on your interval, then one Set node builds the ISO-8601 start date and page size for the Twilio query.",
        "height": 384,
        "width": 560,
        "color": 7
      },
      "id": "4da464d3-79be-4783-8d92-199ee9ecc0b2",
      "name": "Stage Alert Window",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -160,
        576
      ]
    },
    {
      "parameters": {
        "content": "## Fetch alerts and stop when there are none\n\nThe HTTP Request node GETs the Monitor Alerts API with that window, and an IF node ends the run when the alerts array comes back empty.",
        "height": 384,
        "width": 560,
        "color": 7
      },
      "id": "d63787f0-0376-4056-a56c-a5e0a952a3cb",
      "name": "Stage Fetch and Guard",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        464,
        576
      ]
    },
    {
      "parameters": {
        "content": "## Group by error code and severity\n\nA Code node flattens every alert into a row, then collapses the set into one entry per error code and log level with a count and one sample message.",
        "height": 384,
        "width": 432,
        "color": 7
      },
      "id": "7ec35146-65e8-44f6-ae99-da3daff88893",
      "name": "Stage Grouping",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1088,
        544
      ]
    },
    {
      "parameters": {
        "content": "## Log the rows and post one digest\n\nGoogle Sheets appends one row per alert as the audit trail, and Slack gets a single grouped digest built from the counts.",
        "height": 608,
        "width": 496,
        "color": 7
      },
      "id": "658e5c10-4e34-4f68-b9de-131edf39205a",
      "name": "Stage Outputs",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1600,
        544
      ]
    }
  ],
  "connections": {
    "Check Twilio on a Schedule": {
      "main": [
        [
          {
            "node": "Compute Alert Window",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Alert Window": {
      "main": [
        [
          {
            "node": "Fetch Twilio Debugger Alerts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Twilio Debugger Alerts": {
      "main": [
        [
          {
            "node": "Any Alerts Returned?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any Alerts Returned?": {
      "main": [
        [
          {
            "node": "Group Alerts by Code and Severity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group Alerts by Code and Severity": {
      "main": [
        [
          {
            "node": "Append Alert Rows to Sheet",
            "type": "main",
            "index": 0
          },
          {
            "node": "Post Grouped Digest to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  }
}