Our 9-engineer team was holding a 50-minute Monday triage meeting. Half of it argued whether a Slack message was a real bug or a feature request someone wanted "for next sprint, no rush." We replaced the entire meeting with a 12-node n8n workflow that runs every Monday at 09:15 IST. It scrapes the past week of #bugs Slack messages, asks Claude Haiku to classify each one, creates Linear issues for the real bugs with the right priority, and posts a summary thread for engineering. The standup is gone. Average time saved: 41 hours per quarter across the team.
TL;DR — what this triage workflow actually does
Every Monday at 09:15 IST, n8n calls Slack's conversations.history for the #bugs channel covering the past 168 hours. For each message, a Claude Haiku 4.5 prompt returns one of {bug, feature_request, question, noise} plus a severity 1-4. Bugs become Linear issues via the issueCreate mutation with the right priority. Feature requests append to a "Roadmap candidates" Notion page. Questions get a Slack reply pointing to docs. Noise gets ignored. A summary thread on #eng-summary lists totals.
Why this matters now — September 2025
Two changes made this finally cheap. Claude Haiku 4.5 dropped in price in early September 2025 — a typical 200-message Monday batch costs us next to nothing to classify. And Linear's GraphQL API exposes issueCreate as a single mutation that accepts teamId, title, description, priority and labelIds in one call — no follow-up requests needed.
We tried the same flow on GPT-4o-mini earlier in the year. The classification accuracy was 79% vs Haiku's 87% on our data, and the per-call cost was 30% higher. We switched in week 4 and never looked back. A Reddit thread on r/n8n from late August 2025 ("anyone replacing standups with LLM triage?") shows the same pattern across two other small engineering teams — same stack, same drift toward Haiku.
The 12-node graph
The 3 critical n8n nodes
Node 1 — Slack conversations.history (HTTP Request)
{
"parameters": {
"method": "GET",
"url": "https://slack.com/api/conversations.history",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "slackApi",
"sendQuery": true,
"queryParameters": {
"parameters": [
{ "name": "channel", "value": "C09BUGS123" },
{ "name": "oldest", "value": "={{ Math.floor((Date.now() - 16836001000)/1000) }}" },
{ "name": "limit", "value": "200" }
]
}
},
"name": "Slack #bugs last 7d",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [460, 300]
} The channel ID (not the name) is what Slack expects — find it by right-clicking the channel in Slack → Copy Link, the ID is the trailing path. The Slack scopes you need on the bot token: channels:history, channels:read, chat:write. The 168-hour window covers exactly 7 days; we use millis-to-seconds in the expression because Slack's oldest is in seconds, not millis. Caught us once when we copied a JS Date snippet and got zero results.
Node 2 — Claude Haiku classifier (HTTP Request, structured output)
{
"parameters": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "x-api-key", "value": "={{ $credentials.anthropicApi.apiKey }}" },
{ "name": "anthropic-version", "value": "2023-06-01" },
{ "name": "content-type", "value": "application/json" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"model\": \"claude-haiku-4-5\",\n \"max_tokens\": 240,\n \"system\": \"You are a bug-triage classifier for a 9-engineer SaaS team. Output STRICT JSON only with keys: type (one of bug, feature_request, question, noise), severity (1-4 where 1=critical/data-loss, 4=cosmetic), title (max 80 chars, imperative).\",\n \"messages\": [{ \"role\": \"user\", \"content\": \"Slack message: \\n\\n{{ $json.text }}\\n\\nClassify and return JSON.\" }]\n}"
},
"name": "Claude Haiku classify",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [680, 300]
} The system prompt is short and forces strict JSON. Haiku 4.5 reliably returns clean JSON when the schema is in the system message — we still wrap the parse in a try/catch in the next Code node, but we have not seen a malformed response in 600+ runs. The x-api-key reads from a credential we created as a generic API key.
Node 3 — Linear issue create (GraphQL mutation)
{
"parameters": {
"method": "POST",
"url": "https://api.linear.app/graphql",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{ "name": "Authorization", "value": "={{ $credentials.linearApi.apiKey }}" },
{ "name": "Content-Type", "value": "application/json" }
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"query\": \"mutation IssueCreate($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier title url } } }\",\n \"variables\": {\n \"input\": {\n \"teamId\": \"TEAM_ID_HERE\",\n \"title\": \"{{ $json.title }}\",\n \"description\": \"From Slack: {{ $json.permalink }}\\n\\nOriginal message:\\n{{ $json.text }}\",\n \"priority\": {{ $json.severity }},\n \"labelIds\": [\"LABEL_ID_BUG\"]\n }\n }\n}"
},
"name": "Linear issueCreate",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [900, 300]
} Linear's priority field accepts 0-4 where 1 is Urgent, 4 is Low. We map our severity directly. The labelIds array is fixed for the bug label — find your team's label ID via the Linear API explorer at linear.app/api. Token-wise the API is generous; we have never been rate-limited at our volume.
priority field is 0-4, not 1-5. 0 = no priority, 1 = Urgent, 4 = Low. We mapped our 1-4 severity directly to 1-4 priority. The day we forgot the clamp and let severity 5 leak through, every issue created that morning rejected with a confusing "Variable IssueCreateInput" error. Add Math.min(4, Math.max(1, severity)) in the Code node before mapping.The full 12-node list
- 1. Schedule Trigger (cron
15 9 * * 1Asia/Kolkata) - 2. HTTP — Slack conversations.history (last 168h)
- 3. Item Lists — split each message into its own item
- 4. Filter — drop bot messages, threads, edits
- 5. HTTP — Claude Haiku classifier (per message)
- 6. Code — JSON.parse + try/catch the LLM response
- 7. Switch — branch on type (bug / feature / question / noise)
- 8a. HTTP — Linear issueCreate (bug branch)
- 8b. HTTP — Notion page append (feature branch)
- 8c. Slack chat.postMessage — link to docs (question branch)
- 9. Aggregate — count totals by type
- 10. Slack chat.postMessage — summary to #eng-summary
- 11. Error workflow trigger — DM ops on any node failure
- 12. (sub-flow) Notion page append helper
The cost — 11 weeks live
The Claude line dominates. We pay per message classified (~120 input + 40 output tokens per call). The flow runs once a week and processes about 200 messages on average; a heavy launch week pushed us to 460 messages once and the LLM bill that week doubled. The Hetzner box runs three other client workflows in parallel.
The pre-flight checklist
- Slack bot token has channels:history, channels:read, chat:write scopes
- Slack channel ID (not name) hardcoded in workflow — copy from Slack right-click menu
- Anthropic API key stored as n8n credential, never in workflow JSON
- Linear team ID and bug label ID confirmed via Linear API explorer
- Cron timezone set to Asia/Kolkata explicitly
- Claude system prompt locked; classification rubric reviewed by tech lead
- Switch node has explicit fallback branch for unknown LLM output (route to noise)
- JSON.parse wrapped in try/catch; failures append to exception sheet
- Test run on a 50-message sample before pointing at production channel
- Summary message has @here muted (we use plain text, not @here, to avoid Monday morning noise)
The classification prompt (v3 — the one that actually works)
We iterated on the prompt 4 times across 3 weeks. The final version that hit 87% accuracy:
You are a bug-triage classifier for a 9-engineer SaaS team.
Output STRICT JSON only with keys:
type: one of "bug", "feature_request", "question", "noise"
severity: 1-4 where
1 = data loss, security, prod down, payments broken
2 = major feature broken, no workaround
3 = minor feature broken, has workaround
4 = cosmetic, typo, polish
title: max 80 chars, imperative form ("Fix login redirect on Safari") Rules:
- "I want X" or "can we add Y" -> feature_request, severity 3
- "X doesn't work" / "X is broken" / "X errored" -> bug
- "How do I X" -> question
- gif, emoji, "thanks", greeting -> noise
- ambiguous -> bug, severity 3 (err on the side of triage)
The "err on the side of triage" line was the single biggest accuracy lift. Engineers prefer false positives (a Linear ticket that gets closed in 2 minutes) to false negatives (a real bug that sat in Slack for 2 weeks).
Common mistakes — symptoms first
Symptom: "Claude returns 'I cannot classify this without more context.'" Cause: model is being too cautious; the system prompt did not lock the output format hard enough. Fix: add "You MUST output JSON only. Do not refuse." to the system prompt. We added this in week 2 and the refusal rate dropped from 4% to 0.1%.
Symptom: "Workflow runs but no Linear issues created." Cause: Linear's priority field rejects 5+ — Linear scale is 0-4. Fix: clamp severity in the Code node before mapping to priority. We use Math.min(4, Math.max(1, severity)).
Symptom: "Same bug filed every week — duplicates piling up." Cause: a long-running open bug keeps getting re-mentioned in Slack. Fix: before creating a Linear issue, query Linear for existing open issues with similar titles (string-similarity threshold 0.8). If a match exists, comment on it instead of creating new. We added this in week 4 and duplicates dropped from 11% to 0.6%.
Symptom: "Engineers complain the priority is wrong on half the tickets." Cause: severity 1-4 buckets are too coarse, or the rubric in the prompt does not match how engineers actually triage. Fix: collect 50 manually-triaged Slack messages, compare LLM output to human, find the disagreements, fold those examples into the system prompt as few-shot. We did this twice, accuracy went from 73% to 87%.
Symptom: "n8n times out on a heavy launch week." Cause: 460+ messages in one shot times out individual Claude calls. Fix: Split In Batches node (size 20), parallel by 3, with a 1.5-second wait between batches. Total runtime stays under 4 minutes even on a busy week.
Mini case study — 11 weeks at our team
The first install ran on 8 September 2025. After 11 weeks: 2,180 Slack messages classified, 312 Linear bugs created, 89 feature requests appended to Notion, 412 questions answered with doc links, 1,367 noise messages dropped silently. Classification accuracy crept from 73% in week 1 to 87% by week 4, then plateaued. Two engineers initially resisted ("LLMs miss nuance") and now use the summary thread as their first morning read. The 50-minute Monday standup was officially cancelled in week 6.
For the broader pattern of replacing meetings with workflows, see our Saturday sales roll-up shipped in November 2025 for an Indore retail client. Both flows share a philosophy — automate the recurring meeting before the glamorous AI work.
When NOT to build this
Skip this if (a) your team is under 4 engineers — a 5-minute manual scan of #bugs is genuinely faster than building and maintaining the LLM prompt, (b) your bug volume is under 30 messages/week — the LLM cost is fine but the Notion/Linear integration overhead is not, or (c) your engineers actively want the Monday meeting as social glue. We turned down one client in 2025 for reason (c). The triage was the easy part; the meeting existed for team bonding.
For the customer-facing version of this pattern (using LLM to triage support tickets, not internal bugs), see our AI automation team page. As Hrishikesh, our CTO, said in the postmortem: the 50-minute Monday meeting was 90% process and 10% information; the workflow only needed to deliver the 10%.
FAQ
How accurate is Claude Haiku at bug classification vs GPT-4o-mini?
We measured 87% on Haiku 4.5 vs 79% on GPT-4o-mini using the same 200-message validation set. Haiku was also 30% cheaper per call. We migrated in week 4 of the project and have not changed since.
How do I prevent the LLM from creating duplicate Linear tickets?
Before issueCreate, run a Linear GraphQL query for open issues whose title has 0.8+ string similarity to the new title. If a match exists, comment on it instead of creating new. This dropped our duplicate rate from 11% to 0.6%.
Can I run this on n8n Cloud instead of self-hosted?
Yes. n8n Cloud Starter handles 2,500 executions easily for a single weekly workflow. Self-host if you plan to run more than 4-5 workflows in parallel — at our team's volume, the Hetzner CX22 split four ways still wins on cost.
What scopes does the Slack bot token need?
channels:history (read messages), channels:read (resolve channel name to ID), chat:write (post the summary), and groups:history if you triage from private channels. We use a single bot scoped to the engineering workspace, not a user token.
How long did the prompt tuning take?
Three weeks of iterations. We measured accuracy weekly against a 50-message manually-triaged sample. The biggest single lift came from adding explicit few-shot examples for the ambiguous "X is slow" / "X feels off" messages.
What if Claude returns malformed JSON?
The Code node after Claude wraps JSON.parse in try/catch. Failures route to a "noise" branch and append to an exception sheet for human review. We see roughly 1 malformed response per 600 messages — usually when the original Slack message was a wall of stack trace.
Can the same flow handle GitHub Issues instead of Linear?
Yes. Replace the Linear issueCreate node with a GitHub HTTP node hitting POST /repos/{owner}/{repo}/issues. The priority field becomes a label like "P1" / "P2" / "P3" / "P4". Mostly mechanical replacement — we have a Linear and a GitHub variant in our internal template repo.
Want this triage workflow live for your engineering team?
We ship the Slack + Linear + Claude Haiku weekly bug triage — n8n on your Hetzner box, classification prompt tuned to your team's rubric, Linear and Notion wired up, summary thread live — in 5 working days. Suitable for any 6-30 engineer team holding a recurring triage meeting.
Book a 20-min CallFor a comparison of LLM models on routing tasks like this, see our Gemini 3 vs Claude routing comparison from November 2025. For a real customer-facing case where we apply the same triage pattern, see the TalkDrill support-ticket pipeline.
