From 2dc3c66bb463cd983453d1dc69762b5e2c5283ca Mon Sep 17 00:00:00 2001 From: William Valentin Date: Wed, 13 May 2026 14:31:41 -0700 Subject: [PATCH] Add n8n Failure Digest workflow with Discord delivery Workflow G9ylNbHbnJ6fWX2C now sends failure digests to both: - Telegram (existing) - Discord #ops-alerts channel (new) Discord delivery uses Bot Auth (UgPqYcoCNNIgr55m) via HTTP Request node POSTing to Discord API v10 channel messages endpoint. Task: t_627466f8 --- swarm-common/n8n-workflows/G9ylNbHbnJ6fWX2C.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 swarm-common/n8n-workflows/G9ylNbHbnJ6fWX2C.json diff --git a/swarm-common/n8n-workflows/G9ylNbHbnJ6fWX2C.json b/swarm-common/n8n-workflows/G9ylNbHbnJ6fWX2C.json new file mode 100644 index 0000000..7962dd1 --- /dev/null +++ b/swarm-common/n8n-workflows/G9ylNbHbnJ6fWX2C.json @@ -0,0 +1 @@ +{"updatedAt":"2026-05-12T17:06:07.191Z","createdAt":"2026-05-12T16:59:40.394Z","id":"G9ylNbHbnJ6fWX2C","name":"n8n Failure Digest","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{},"type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[-920,-120],"id":"a673b342-0e9e-44ae-a470-0a7ba93d135e","name":"Manual Trigger"},{"parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"0 10 * * * *"}]}},"type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.3,"position":[-920,80],"id":"6b8a395f-eadd-479d-980d-6f744f411c7d","name":"Hourly Schedule"},{"parameters":{"url":"http://127.0.0.1:5678/api/v1/executions?status=error&limit=100","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-660,0],"id":"afbf364e-4aca-4c7f-a43a-62a5e0b05d3b","name":"List Failed Executions","credentials":{"httpHeaderAuth":{"id":"UPAHgUJVRqZQceL4","name":"n8n Public API (Failure Digest)"}}},{"parameters":{"mode":"runOnceForAllItems","jsCode":"const data = Array.isArray($json.data) ? $json.data : [];\nconst windowMinutes = 65;\nconst cutoff = Date.now() - windowMinutes * 60 * 1000;\nconst selfName = 'n8n Failure Digest';\nconst seen = new Set();\nconst out = [];\nfor (const ex of data) {\n const status = String(ex.status || '').toLowerCase();\n if (!['error', 'crashed'].includes(status)) continue;\n const t = Date.parse(ex.stoppedAt || ex.startedAt || ex.createdAt || '');\n if (Number.isFinite(t) && t < cutoff) continue;\n const id = String(ex.id || '');\n if (!id || seen.has(id)) continue;\n seen.add(id);\n out.push({ json: { id, status, startedAt: ex.startedAt, stoppedAt: ex.stoppedAt, workflowId: ex.workflowId, windowMinutes } });\n}\nreturn out;"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[-420,0],"id":"00f4d7aa-3890-4eb4-bcb4-64afd7675767","name":"Recent Failure IDs"},{"parameters":{"url":"=http://127.0.0.1:5678/api/v1/executions/{{$json.id}}?includeData=true","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-180,0],"id":"4de4125e-75d6-4896-93d1-1ce20dce2db8","name":"Fetch Failure Details","credentials":{"httpHeaderAuth":{"id":"UPAHgUJVRqZQceL4","name":"n8n Public API (Failure Digest)"}}},{"parameters":{"mode":"runOnceForAllItems","jsCode":"const items = $input.all();\nconst windowMinutes = 65;\nconst now = Date.now();\nconst selfName = 'n8n Failure Digest';\nfunction arr(v) { return Array.isArray(v) ? v : (v == null ? [] : [v]); }\nfunction msg(err) {\n if (!err) return 'Unknown error';\n return String(err.message || err.description || err.name || err.code || JSON.stringify(err)).trim() || 'Unknown error';\n}\nfunction errType(err) { return String(err?.name || err?.type || err?.code || err?.httpCode || 'Error'); }\nfunction sig(s) {\n return String(s).split('\\n')[0]\n .replace(/https?:\\/\\/\\S+/g, '')\n .replace(/[0-9a-f]{8,}/gi, '')\n .replace(/\\b\\d{4,}\\b/g, '')\n .slice(0, 180);\n}\nfunction findErr(ex) {\n const rd = ex.data?.resultData || {};\n if (rd.error) return { node: rd.error.node?.name || rd.error.node || rd.lastNodeExecuted || 'unknown', error: rd.error };\n const runData = rd.runData || {};\n for (const [nodeName, attempts] of Object.entries(runData)) {\n for (const attempt of arr(attempts).slice().reverse()) {\n if (attempt?.error) return { node: nodeName, error: attempt.error };\n }\n }\n return { node: rd.lastNodeExecuted || 'unknown', error: ex.error || {} };\n}\nconst failures = [];\nfor (const item of items) {\n const ex = item.json || {};\n const workflowName = ex.workflowData?.name || ex.workflow?.name || `Workflow ${ex.workflowId || 'unknown'}`;\n if (workflowName === selfName) continue;\n const found = findErr(ex);\n const message = msg(found.error);\n const when = ex.stoppedAt || ex.startedAt || ex.createdAt || new Date(now).toISOString();\n failures.push({\n id: ex.id,\n workflowId: ex.workflowId || ex.workflowData?.id || 'unknown',\n workflowName,\n node: found.node || 'unknown',\n errorType: errType(found.error),\n message,\n signature: sig(message),\n when,\n status: ex.status || 'unknown',\n });\n}\nconst groups = new Map();\nfor (const f of failures) {\n const key = `${f.workflowId}\\u0000${f.node}\\u0000${f.errorType}\\u0000${f.signature}`;\n if (!groups.has(key)) groups.set(key, { workflowName: f.workflowName, workflowId: f.workflowId, node: f.node, errorType: f.errorType, signature: f.signature, count: 0, ids: [], latest: f.when });\n const g = groups.get(key);\n g.count++;\n if (g.ids.length < 8) g.ids.push(f.id);\n if (String(f.when) > String(g.latest)) g.latest = f.when;\n}\nconst sorted = [...groups.values()].sort((a,b) => b.count - a.count || String(b.latest).localeCompare(String(a.latest))).slice(0, 12);\nif (!sorted.length) return [];\nconst lines = [];\nlines.push(`🚨 n8n Failure Digest: ${failures.length} failed execution(s) in the last ${windowMinutes} min`);\nlines.push('');\nsorted.forEach((g, i) => {\n lines.push(`${i+1}. ${g.workflowName}`);\n lines.push(` Node: ${g.node}`);\n lines.push(` ${g.count}x ${g.errorType}: ${g.signature}`);\n lines.push(` Execs: ${g.ids.join(', ')} | latest ${g.latest}`);\n});\nlines.push('');\nlines.push('Open n8n: http://127.0.0.1:18808');\nreturn [{ json: { text: lines.join('\\n'), totalFailures: failures.length, groups: sorted, generatedAt: new Date(now).toISOString() } }];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[80,0],"id":"f6b4eab8-7017-43e6-97c8-dce63873e097","name":"Build Digest"},{"parameters":{"chatId":"8367012007","text":"={{ $json.text }}","additionalFields":{"parse_mode":"Markdown"}},"type":"n8n-nodes-base.telegram","typeVersion":1,"position":[340,0],"id":"cf49d05d-5d81-404b-a751-ce56794985a9","name":"Send Telegram Digest","credentials":{"telegramApi":{"id":"aox4dyIWVSRdcH5z","name":"Telegram Bot (OpenClaw)"}}},{"parameters":{"method":"POST","url":"https://discord.com/api/v10/channels/1494453542243532932/messages","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ content: $json.text.substring(0, 2000) }) }}","options":{"response":{"response":{"responseFormat":"text"}}}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[340,200],"id":"6c3086e4-0869-4003-94c3-66b4975f94e9","name":"Send Discord Digest","credentials":{"httpHeaderAuth":{"id":"UgPqYcoCNNIgr55m","name":"Discord Bot Auth"}}}],"connections":{"Manual Trigger":{"main":[[{"node":"List Failed Executions","type":"main","index":0}]]},"Hourly Schedule":{"main":[[{"node":"List Failed Executions","type":"main","index":0}]]},"List Failed Executions":{"main":[[{"node":"Recent Failure IDs","type":"main","index":0}]]},"Recent Failure IDs":{"main":[[{"node":"Fetch Failure Details","type":"main","index":0}]]},"Fetch Failure Details":{"main":[[{"node":"Build Digest","type":"main","index":0}]]},"Build Digest":{"main":[[{"node":"Send Telegram Digest","type":"main","index":0},{"node":"Send Discord Digest","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","timezone":"America/Los_Angeles","saveDataErrorExecution":"all","saveDataSuccessExecution":"none","callerPolicy":"workflowsFromSameOwner","availableInMCP":false},"staticData":{"node:Hourly Schedule":{"recurrenceRules":[]}},"meta":null,"pinData":null,"versionId":"c198c473-6fed-4fa8-b203-9465bb084e89","activeVersionId":"c198c473-6fed-4fa8-b203-9465bb084e89","versionCounter":19,"triggerCount":1,"shared":[{"updatedAt":"2026-05-12T16:59:40.395Z","createdAt":"2026-05-12T16:59:40.395Z","role":"workflow:owner","workflowId":"G9ylNbHbnJ6fWX2C","projectId":"WGdp8QunI1tHpjXa","project":{"updatedAt":"2026-03-11T21:08:10.005Z","createdAt":"2026-03-11T21:05:11.541Z","id":"WGdp8QunI1tHpjXa","name":"will will ","type":"personal","icon":null,"description":null,"creatorId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}}],"tags":[],"activeVersion":{"updatedAt":"2026-05-13T21:29:18.280Z","createdAt":"2026-05-13T21:29:18.280Z","versionId":"c198c473-6fed-4fa8-b203-9465bb084e89","workflowId":"G9ylNbHbnJ6fWX2C","nodes":[{"parameters":{},"type":"n8n-nodes-base.manualTrigger","typeVersion":1,"position":[-920,-120],"id":"a673b342-0e9e-44ae-a470-0a7ba93d135e","name":"Manual Trigger"},{"parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"0 10 * * * *"}]}},"type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.3,"position":[-920,80],"id":"6b8a395f-eadd-479d-980d-6f744f411c7d","name":"Hourly Schedule"},{"parameters":{"url":"http://127.0.0.1:5678/api/v1/executions?status=error&limit=100","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-660,0],"id":"afbf364e-4aca-4c7f-a43a-62a5e0b05d3b","name":"List Failed Executions","credentials":{"httpHeaderAuth":{"id":"UPAHgUJVRqZQceL4","name":"n8n Public API (Failure Digest)"}}},{"parameters":{"mode":"runOnceForAllItems","jsCode":"const data = Array.isArray($json.data) ? $json.data : [];\nconst windowMinutes = 65;\nconst cutoff = Date.now() - windowMinutes * 60 * 1000;\nconst selfName = 'n8n Failure Digest';\nconst seen = new Set();\nconst out = [];\nfor (const ex of data) {\n const status = String(ex.status || '').toLowerCase();\n if (!['error', 'crashed'].includes(status)) continue;\n const t = Date.parse(ex.stoppedAt || ex.startedAt || ex.createdAt || '');\n if (Number.isFinite(t) && t < cutoff) continue;\n const id = String(ex.id || '');\n if (!id || seen.has(id)) continue;\n seen.add(id);\n out.push({ json: { id, status, startedAt: ex.startedAt, stoppedAt: ex.stoppedAt, workflowId: ex.workflowId, windowMinutes } });\n}\nreturn out;"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[-420,0],"id":"00f4d7aa-3890-4eb4-bcb4-64afd7675767","name":"Recent Failure IDs"},{"parameters":{"url":"=http://127.0.0.1:5678/api/v1/executions/{{$json.id}}?includeData=true","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-180,0],"id":"4de4125e-75d6-4896-93d1-1ce20dce2db8","name":"Fetch Failure Details","credentials":{"httpHeaderAuth":{"id":"UPAHgUJVRqZQceL4","name":"n8n Public API (Failure Digest)"}}},{"parameters":{"mode":"runOnceForAllItems","jsCode":"const items = $input.all();\nconst windowMinutes = 65;\nconst now = Date.now();\nconst selfName = 'n8n Failure Digest';\nfunction arr(v) { return Array.isArray(v) ? v : (v == null ? [] : [v]); }\nfunction msg(err) {\n if (!err) return 'Unknown error';\n return String(err.message || err.description || err.name || err.code || JSON.stringify(err)).trim() || 'Unknown error';\n}\nfunction errType(err) { return String(err?.name || err?.type || err?.code || err?.httpCode || 'Error'); }\nfunction sig(s) {\n return String(s).split('\\n')[0]\n .replace(/https?:\\/\\/\\S+/g, '')\n .replace(/[0-9a-f]{8,}/gi, '')\n .replace(/\\b\\d{4,}\\b/g, '')\n .slice(0, 180);\n}\nfunction findErr(ex) {\n const rd = ex.data?.resultData || {};\n if (rd.error) return { node: rd.error.node?.name || rd.error.node || rd.lastNodeExecuted || 'unknown', error: rd.error };\n const runData = rd.runData || {};\n for (const [nodeName, attempts] of Object.entries(runData)) {\n for (const attempt of arr(attempts).slice().reverse()) {\n if (attempt?.error) return { node: nodeName, error: attempt.error };\n }\n }\n return { node: rd.lastNodeExecuted || 'unknown', error: ex.error || {} };\n}\nconst failures = [];\nfor (const item of items) {\n const ex = item.json || {};\n const workflowName = ex.workflowData?.name || ex.workflow?.name || `Workflow ${ex.workflowId || 'unknown'}`;\n if (workflowName === selfName) continue;\n const found = findErr(ex);\n const message = msg(found.error);\n const when = ex.stoppedAt || ex.startedAt || ex.createdAt || new Date(now).toISOString();\n failures.push({\n id: ex.id,\n workflowId: ex.workflowId || ex.workflowData?.id || 'unknown',\n workflowName,\n node: found.node || 'unknown',\n errorType: errType(found.error),\n message,\n signature: sig(message),\n when,\n status: ex.status || 'unknown',\n });\n}\nconst groups = new Map();\nfor (const f of failures) {\n const key = `${f.workflowId}\\u0000${f.node}\\u0000${f.errorType}\\u0000${f.signature}`;\n if (!groups.has(key)) groups.set(key, { workflowName: f.workflowName, workflowId: f.workflowId, node: f.node, errorType: f.errorType, signature: f.signature, count: 0, ids: [], latest: f.when });\n const g = groups.get(key);\n g.count++;\n if (g.ids.length < 8) g.ids.push(f.id);\n if (String(f.when) > String(g.latest)) g.latest = f.when;\n}\nconst sorted = [...groups.values()].sort((a,b) => b.count - a.count || String(b.latest).localeCompare(String(a.latest))).slice(0, 12);\nif (!sorted.length) return [];\nconst lines = [];\nlines.push(`🚨 n8n Failure Digest: ${failures.length} failed execution(s) in the last ${windowMinutes} min`);\nlines.push('');\nsorted.forEach((g, i) => {\n lines.push(`${i+1}. ${g.workflowName}`);\n lines.push(` Node: ${g.node}`);\n lines.push(` ${g.count}x ${g.errorType}: ${g.signature}`);\n lines.push(` Execs: ${g.ids.join(', ')} | latest ${g.latest}`);\n});\nlines.push('');\nlines.push('Open n8n: http://127.0.0.1:18808');\nreturn [{ json: { text: lines.join('\\n'), totalFailures: failures.length, groups: sorted, generatedAt: new Date(now).toISOString() } }];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[80,0],"id":"f6b4eab8-7017-43e6-97c8-dce63873e097","name":"Build Digest"},{"parameters":{"chatId":"8367012007","text":"={{ $json.text }}","additionalFields":{"parse_mode":"Markdown"}},"type":"n8n-nodes-base.telegram","typeVersion":1,"position":[340,0],"id":"cf49d05d-5d81-404b-a751-ce56794985a9","name":"Send Telegram Digest","credentials":{"telegramApi":{"id":"aox4dyIWVSRdcH5z","name":"Telegram Bot (OpenClaw)"}}},{"parameters":{"method":"POST","url":"https://discord.com/api/v10/channels/1494453542243532932/messages","authentication":"genericCredentialType","genericAuthType":"httpHeaderAuth","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ content: $json.text.substring(0, 2000) }) }}","options":{"response":{"response":{"responseFormat":"text"}}}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[340,200],"id":"6c3086e4-0869-4003-94c3-66b4975f94e9","name":"Send Discord Digest","credentials":{"httpHeaderAuth":{"id":"UgPqYcoCNNIgr55m","name":"Discord Bot Auth"}}}],"connections":{"Manual Trigger":{"main":[[{"node":"List Failed Executions","type":"main","index":0}]]},"Hourly Schedule":{"main":[[{"node":"List Failed Executions","type":"main","index":0}]]},"List Failed Executions":{"main":[[{"node":"Recent Failure IDs","type":"main","index":0}]]},"Recent Failure IDs":{"main":[[{"node":"Fetch Failure Details","type":"main","index":0}]]},"Fetch Failure Details":{"main":[[{"node":"Build Digest","type":"main","index":0}]]},"Build Digest":{"main":[[{"node":"Send Telegram Digest","type":"main","index":0},{"node":"Send Discord Digest","type":"main","index":0}]]}},"authors":"import","name":null,"description":null,"autosaved":false,"workflowPublishHistory":[{"createdAt":"2026-05-13T21:30:20.994Z","id":1429,"workflowId":"G9ylNbHbnJ6fWX2C","versionId":"c198c473-6fed-4fa8-b203-9465bb084e89","event":"activated","userId":"5ad50ead-6e6a-4d12-ab5b-e5db15835bb5"}]}} \ No newline at end of file