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 to $1/$5 per million tokens in early September 2025 — a typical 200-message Monday batch costs us under ₹3 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
{
"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. Cost: roughly ₹0.012 per message at September 2025 prices, so a busy 200-message week costs ₹2.40 in LLM tokens.
### 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.- 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
- 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)
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 at $24/month 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 for ₹38,000. Suitable for any 6-30 engineer team holding a recurring triage meeting.
Book a 20-min Call
