{"slug": "build-an-ai-agent-error-triage-pipeline-with-n8n-gemini-slack", "title": "Build an AI Agent Error Triage Pipeline with n8n, Gemini & Slack", "summary": "A developer detailed a production-ready n8n workflow for triaging AI agent errors, which deduplicates alerts, classifies failures into five categories using Google Gemini, and posts severity-scored reports to Slack. The pipeline integrates with Sentry and generic webhooks, and includes a suppression window to reduce alert fatigue.", "body_md": "When LLM agents hit production traffic, manual error triage stops scaling fast. The same five failure types keep appearing under different traces, each re-diagnosed from scratch. Duplicate alerts flood Slack. Engineers start muting the channel.\n\nThis post walks through a production-ready n8n workflow that reads a raw agent error, deduplicates it, classifies it against five real failure categories using Gemini, attaches a fix playbook, and posts a severity-scored triage report to Slack — without forcing a classification when the model isn't confident.\n\nFree workflow JSON at the end.\n\nLLM agent failures cluster into a small number of repeatable categories:\n\n| Category | Description | \n|---|---|\n| **Tool Call Failure** | External API, database, or function call failed outright | \n| **Context Window Exhaustion** | Conversation or tool history exceeded model context limit | \n| **State Corruption** | Agent memory/task state in unanticipated condition | \n| **Retry Loop** | Agent stuck repeating the same failed step without progress | \n| **Uncertain** | Error doesn't cleanly fit any named category | \n\nThe problem: nothing remembers having seen these before. Same failure type, fresh diagnosis, every time. One bug generates dozens of identical alerts. Without severity signals, everything looks equally urgent — which means nothing is — and engineers tune out.\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│              n8n AI Agent Triage Workflow                    │\n│                                                             │\n│  [Webhook: POST /agent-error-triage]                        │\n│       ↓                                                     │\n│  Accepts: Sentry native payload OR { message: \"...\" }       │\n│       ↓                                                     │\n│  [Extract & Dedup Code Node]                                │\n│       → Normalize payload (title, culprit, stack_trace)     │\n│       → Build fingerprint (hash of identifying fields)      │\n│       → Check 30-min in-memory suppression window           │\n│       ↓              ↓                                      │\n│  [isDuplicate=true]  [isDuplicate=false]                    │\n│       ↓                    ↓                                │\n│  [Skip No-Op]    [Classify with Gemini]                     │\n│                       ↓                                     │\n│              Structured Output Parser                        │\n│              → { category, confidence, severity,            │\n│                  summary, reasoning }                        │\n│                       ↓                                     │\n│              [Build Triage Report Code Node]                │\n│              → Map category → fix playbook                  │\n│              → Assemble Slack message                       │\n│                       ↓                                     │\n│              [Post Triage Report: Slack Node]               │\n│              → #agent-alerts channel                        │\n└─────────────────────────────────────────────────────────────┘\n```\n\n| Tool | Role | \n|---|---|\n| n8n (self-hosted) | Orchestration | \n| Sentry (optional) | Error source via webhook | \n| Google Gemini | AI classification | \n| LangChain in n8n | Chain + Structured Output Parser | \n| Slack Bot | Triage report delivery | \n\nListens on `POST /agent-error-triage`. Accepts two payload shapes:\n\n**Sentry native webhook:**\n\n```\n{\n  \"data\": {\n    \"event\": {\n      \"title\": \"ToolCallError: Timeout on /api/crm\",\n      \"culprit\": \"agent.tool_executor.call_external\",\n      \"exception\": {\n        \"values\": [{ \"stacktrace\": { \"frames\": [...] } }]\n      }\n    }\n  }\n}\n```\n\n**Generic payload (any error source):**\n\n```\n{\n  \"message\": \"Agent failed: context limit exceeded after 47 tool calls\"\n}\n```\n\nBoth shapes are normalised to the same internal structure in the next node.\n\nThis is the most important node in the pipeline. It does two things:\n\n**Normalise the payload:**\n\n``` js\n// Code Node — Extract & Dedup\nconst input = $input.first().json;\n\n// Handle both Sentry and generic payloads\nlet title, culprit, stackTrace;\n\nif (input.data?.event) {\n  // Sentry native format\n  const event = input.data.event;\n  title = event.title || 'Unknown Error';\n  culprit = event.culprit || '';\n  const frames = event.exception?.values?.[0]?.stacktrace?.frames || [];\n  stackTrace = frames.map(f => `${f.filename}:${f.lineno} in ${f.function}`).join('\\n');\n} else {\n  // Generic format\n  title = input.message || 'Unknown Error';\n  culprit = input.culprit || '';\n  stackTrace = input.stack_trace || '';\n}\n\n// Build fingerprint — hash of identifying fields (not timestamps/IDs)\nconst fingerprintSource = `${title}::${culprit}::${stackTrace.substring(0, 200)}`;\nconst fingerprint = fingerprintSource.split('').reduce((hash, char) => {\n  return ((hash << 5) - hash) + char.charCodeAt(0);\n}, 0).toString(36);\n\n// Check 30-minute suppression window using n8n static data\nconst staticData = $getWorkflowStaticData('global');\nconst seen = staticData.seenFingerprints || {};\nconst now = Date.now();\nconst windowMs = 30 * 60 * 1000; // 30 minutes\n\n// Clean expired entries\nObject.keys(seen).forEach(fp => {\n  if (now - seen[fp] > windowMs) delete seen[fp];\n});\n\nconst isDuplicate = !!seen[fingerprint];\n\nif (!isDuplicate) {\n  seen[fingerprint] = now;\n}\n\nstaticData.seenFingerprints = seen;\n\nreturn [{\n  json: { title, culprit, stackTrace, fingerprint, isDuplicate }\n}];\n```\n\n**Why non-cryptographic hashing:** MD5/SHA isn't available in n8n's sandboxed JS environment by default. The djb2-style hash above is sufficient for fingerprinting — collision probability across a 30-minute window is negligible for error triage purposes.\n\n```\nisDuplicate == true  → Skip (No-Op node, logs fingerprint)\nisDuplicate == false → Continue to classification\n```\n\nThis is what prevents one bug from generating forty identical Slack reports.\n\n```\nYou are an AI agent error classifier. Classify the error into exactly one of these categories:\n\n1. tool_call_failure — A call to an external API, database, function, or tool failed outright. Look for timeout errors, HTTP 4xx/5xx codes, connection refused, or authentication failures in tool calls.\n\n2. context_window_exhaustion — The model's context limit was reached. Look for \"context length exceeded\", \"max tokens\", or errors after long chains of tool calls with no summarisation.\n\n3. state_corruption — The agent's internal memory or task state is in an unexpected condition. Look for key errors on expected state fields, type mismatches in state objects, or missing required state values.\n\n4. retry_loop — The agent is stuck repeating the same failed action without progress. Look for repeated identical tool calls, loop detection errors, or max retry exceeded messages.\n\n5. uncertain — The error doesn't clearly fit any category above. Use this when evidence is ambiguous or the error message lacks enough context to classify confidently.\n\nRules:\n- Choose uncertain rather than force a bad fit\n- Base classification only on evidence in the error, not assumptions\n- Confidence should reflect actual evidence strength, not optimism\n\nReturn ONLY valid JSON:\n{\n  \"category\": \"tool_call_failure|context_window_exhaustion|state_corruption|retry_loop|uncertain\",\n  \"confidence\": 0.0-1.0,\n  \"severity\": \"low|medium|high|critical\",\n  \"summary\": \"One sentence plain-language description of what happened\",\n  \"reasoning\": \"Why you chose this category based on specific evidence in the error\"\n}\n```\n\n**Key settings:**\n\n`0.1` — keeps labels consistent across identical errors regardless of time-of-day`gemini-1.5-flash` or `gemini-1.5-pro` — confirm availability against your live credential before deploying`512` — sufficient for the JSON response\nThe LangChain Structured Output Parser sub-node enforces the JSON schema. If parsing fails (model added markdown fences, malformed JSON, etc.), route to the error branch rather than letting bad output propagate downstream.\n\nMaps category to fix playbook and assembles the Slack message:\n\n```\n// Code Node — Build Triage Report\nconst { category, confidence, severity, summary, reasoning } = $input.first().json;\nconst { title, culprit, stackTrace } = $('Extract & Dedup').first().json;\n\nconst playbooks = {\n  tool_call_failure: [\n    '1. Check external service status page for outages',\n    '2. Review tool call logs for HTTP status codes and timeouts',\n    '3. Verify API credentials and rate limit headers',\n    '4. Add retry with exponential backoff if not already present'\n  ],\n  context_window_exhaustion: [\n    '1. Add conversation summarisation step before context limit is reached',\n    '2. Review tool call chain length — reduce unnecessary calls',\n    '3. Implement sliding window for conversation history',\n    '4. Consider chunking long tool outputs before adding to context'\n  ],\n  state_corruption: [\n    '1. Add state validation checks at each agent step',\n    '2. Review state schema for missing required fields',\n    '3. Check for race conditions in concurrent agent runs',\n    '4. Add state recovery logic or restart from last valid checkpoint'\n  ],\n  retry_loop: [\n    '1. Implement loop detection — track repeated tool call signatures',\n    '2. Set max retry limit with circuit breaker',\n    '3. Add fallback action when retry limit is reached',\n    '4. Review condition for loop exit — is the success criteria reachable?'\n  ],\n  uncertain: [\n    '1. Review full stack trace manually — insufficient context for classification',\n    '2. Improve agent error logging to include more diagnostic detail',\n    '3. Check recent deployments for changes that could explain the failure',\n    '4. Escalate to senior engineer if pattern recurs'\n  ]\n};\n\nconst severityEmoji = {\n  critical: '🔴',\n  high: '🟠',\n  medium: '🟡',\n  low: '🟢'\n};\n\nconst confidenceBar = confidence >= 0.8 ? 'High' : confidence >= 0.5 ? 'Medium' : 'Low';\nconst playbook = playbooks[category] || playbooks.uncertain;\n\n// Build Slack Block Kit message\nconst blocks = [\n  {\n    type: 'header',\n    text: {\n      type: 'plain_text',\n      text: `${severityEmoji[severity] || '⚪'} AI Agent Error Triage Report`\n    }\n  },\n  {\n    type: 'section',\n    fields: [\n      { type: 'mrkdwn', text: `*Category:*\\n${category.replace(/_/g, ' ').toUpperCase()}` },\n      { type: 'mrkdwn', text: `*Severity:*\\n${severity.toUpperCase()}` },\n      { type: 'mrkdwn', text: `*Confidence:*\\n${confidenceBar} (${(confidence * 100).toFixed(0)}%)` },\n      { type: 'mrkdwn', text: `*Culprit:*\\n${culprit || 'Unknown'}` }\n    ]\n  },\n  {\n    type: 'section',\n    text: { type: 'mrkdwn', text: `*Error:*\\n${title}` }\n  },\n  {\n    type: 'section',\n    text: { type: 'mrkdwn', text: `*Summary:*\\n${summary}` }\n  },\n  {\n    type: 'section',\n    text: { type: 'mrkdwn', text: `*AI Reasoning:*\\n${reasoning}` }\n  },\n  {\n    type: 'section',\n    text: {\n      type: 'mrkdwn',\n      text: `*Suggested Fix Steps:*\\n${playbook.join('\\n')}`\n    }\n  },\n  { type: 'divider' }\n];\n\nreturn [{ json: { blocks, category, severity, confidence } }];\n```\n\nConfigure the Slack node with:\n\n`#agent-alerts` channel ID`blocks` array from previous node)\n**Slack bot scoping — important:** The bot token should be limited to `chat:write` in the specific channel. Do not use a token with broad workspace permissions for this integration.\n\nLow confidence or JSON parse failures should not silently drop. Build an error branch:\n\n``` js\n// Error branch — uncertain fallback\nconst errorContext = {\n  timestamp: new Date().toISOString(),\n  stage: $input.first().json.failedNode || 'classification',\n  rawError: $input.first().json.error?.message || 'Unknown',\n  originalTitle: $('Extract & Dedup').first().json?.title || 'Unknown'\n};\n\n// Override with uncertain classification\nreturn [{\n  json: {\n    category: 'uncertain',\n    confidence: 0,\n    severity: 'medium',\n    summary: 'Classification failed — manual review required',\n    reasoning: `Workflow error at ${errorContext.stage}: ${errorContext.rawError}`,\n    ...errorContext\n  }\n}];\n```\n\nRoute this into the Build Triage Report node so the uncertain playbook still goes to Slack — a failed classification is still an incident that needs visibility.\n\nGemini isn't a hard requirement. Replacing it means updating the credential and model node — the Structured Output Parser is model-agnostic as long as the JSON schema is valid. Claude and GPT-4o both work well for structured classification tasks.\n\n**Note on Gemini rate limits:** Free tier limits vary by model version and can change. For production traffic, confirm current limits before relying on the free tier — hitting a rate limit mid-triage means classification requests silently fail.\n\nThe 30-minute window is a starting point. If the same bug tends to resurface after fixes and rollbacks, a shorter window makes sense. If your agents produce very infrequent errors, a longer window reduces noise further.\n\n``` js\nconst windowMs = 30 * 60 * 1000; // Adjust this value\n```\n\nCurrently everything posts to Slack automatically. To add a human gate before high-severity classifications trigger downstream automation:\n\n```\n[Build Triage Report]\n       ↓\n[IF severity == critical]\n       ↓\n[Send approval email: Proceed / Escalate]\n       ↓\n[n8n Wait Node]\n       ↓\n[Webhook: receives decision]\n       ↓\n[Route to downstream automation]\n```\n\nCurrently reports live in Slack only. For trend tracking (which failure category appears most this month, which service generates the most errors):\n\n```\n[Post to Slack]\n       ↓\n[Jira: Create Issue] OR [Linear: Create Issue]\nFields: title=error title, label=category, priority=severity\n```\n\nSentry is one input, not the only one. Any system that can POST a JSON body to the webhook URL — a custom logging pipeline, a different error tracker, CloudWatch alerts, a Datadog webhook — can feed this workflow.\n\n**Classification quality depends on logging quality.** A stack trace with a real culprit and clear exception type gives the model solid evidence. A log entry that says `Exception: failed` gives it nothing — the workflow correctly labels that uncertain rather than guessing. Better logging matters more than classifier tuning for sparse errors.\n\n**No persistent storage for trend analysis.** The dedup fingerprint store uses n8n workflow static data, which is in-memory and resets on workflow restart. For cross-session dedup or trend reporting, connect to a database or sheet instead.\n\n**The uncertain category is a feature.** If you find everything is coming back uncertain, the problem is upstream logging quality, not the classifier threshold.\n\nIT Path Solutions published the complete n8n workflow — webhook trigger, Extract & Dedup node, Gemini classification chain, Structured Output Parser, fix playbook mapping, Slack Block Kit reporting, and error handling — all pre-connected.\n\nImport into any n8n instance, add credentials, send a test error through the webhook.\n\n👉 [Download the free n8n AI agent monitoring workflow](https://www.itpathsolutions.com/ai-agent-monitoring-workflow)\n\nSetup guide covers: Gemini credential config, Slack bot token scoping, Sentry webhook setup, dedup window tuning, and how to test end-to-end before pointing production traffic at it.\n\nThe pipeline works because it does the repetitive diagnostic work — normalisation, dedup, classification, playbook lookup — before a human sees the alert. The on-call engineer opens Slack and sees a structured triage report instead of a raw stack trace to interpret.\n\nThe honest uncertain fallback is what makes it trustworthy: it admits when it doesn't know rather than sending an engineer down the wrong path with a confident wrong answer.\n\nRun one test error through the webhook to confirm the chain works before enabling for production traffic.\n\nFull guide and JSON: [itpathsolutions.com/ai-agent-monitoring-workflow](https://www.itpathsolutions.com/ai-agent-monitoring-workflow)\n\n*Running something similar in production? What failure category shows up most? Drop it in the comments.*", "url": "https://wpnews.pro/news/build-an-ai-agent-error-triage-pipeline-with-n8n-gemini-slack", "canonical_source": "https://dev.to/mateo_ruiz_6992b1fce47843/build-an-ai-agent-error-triage-pipeline-with-n8n-gemini-slack-3kh5", "published_at": "2026-09-09 10:05:33+00:00", "updated_at": "2026-09-09 10:38:59.109152+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "mlops", "artificial-intelligence"], "entities": ["n8n", "Google Gemini", "Slack", "Sentry", "LangChain"], "alternates": {"html": "https://wpnews.pro/news/build-an-ai-agent-error-triage-pipeline-with-n8n-gemini-slack", "markdown": "https://wpnews.pro/news/build-an-ai-agent-error-triage-pipeline-with-n8n-gemini-slack.md", "text": "https://wpnews.pro/news/build-an-ai-agent-error-triage-pipeline-with-n8n-gemini-slack.txt", "jsonld": "https://wpnews.pro/news/build-an-ai-agent-error-triage-pipeline-with-n8n-gemini-slack.jsonld"}}