How I built a keyword-based support ticket categorizer in n8n (no AI API needed)

I wanted to share a pattern I found useful: automatically categorizing support tickets in n8n using a pure keyword-scoring Code node — no OpenAI, no external AI API, no ongoing cost.

**The problem**

Our support webhook was receiving a mix of billing questions, bug reports, feature requests, account issues, and general messages. All arriving in a flat queue. Manually sorting was burning time.

**The approach**

A single Code node scores each incoming message against category-specific keyword lists:

```javascript

const categories = {

billing: [‘invoice’,‘charge’,‘refund’,‘payment’,‘subscription’,‘cancel’],

bug: [‘error’,‘broken’,‘bug’,‘crash’,‘fail’,‘not working’,‘404’,‘500’],

feature: [‘feature’,‘request’,‘suggestion’,‘enhancement’,‘roadmap’],

account: [‘login’,‘password’,‘reset’,‘access’,‘locked’,‘forgot’,‘2fa’]

};

let category = ‘general’, maxScore = 0;

for (const [cat, keywords] of Object.entries(categories)) {

const score = keywords.filter(k => combined.includes(k)).length;

if (score > maxScore) { maxScore = score; category = cat; }

}

```

The category with the most keyword matches wins. Ties default to general.

Priority is set separately — scan for urgency words (`urgent`, `asap`, `crash`, `critical`) → if any match, priority = high. Bug reports without urgency default to medium; everything else defaults to low.

**The full flow**

```

Webhook → Code (categorize + prioritize) → IF (high priority?)

→ HTTP Request (Notion ticket — with [HIGH] prefix if urgent)

→ HTTP Request (auto-reply email, category-specific template)

→ Respond to Webhook

```

**What I learned**

1. The keyword approach performs well for support triage specifically because users use domain vocabulary. “Can’t login”, “invoice”, “not working” — strong signal.

2. Scoring by count (not just presence) handles overlap well. “I can’t login and I need a refund” scores higher for billing (refund) than account (login) — which is probably right.

3. The `combined = subject + ’ ’ + message` concatenation before scoring matters. Subjects alone are often too short for reliable categorization.

**Extending it**

- Add more keywords to any category in the `categories` object

- Add a new category by adding a new key + array

- Swap Notion for Linear, Jira, or any HTTP API

- Add a Slack alert after the IF node for high-priority tickets

Happy to share more about the email templating approach or the Notion schema if useful. What are others using for support triage in n8n?

1 Like

A few improvements that made ours more accurate:

  • Normalize the text first (toLowerCase(), remove punctuation, trim extra spaces) before matching keywords.
  • Use weighted keywords instead of treating every match equally. For example, refund or chargeback can be worth 3 points, while payment is worth 1.
  • Match whole words with regex to avoid false positives (e.g., access shouldn’t accidentally match part of another word).
  • Add exclusion rules. For example, “I can’t log in to cancel my subscription” may need to prioritize account over billing depending on your workflow.
  • Log tickets that fall into general so you can review them weekly and expand your keyword lists. That feedback loop steadily improves accuracy.

We also added a confidence score (highestScore / totalMatches). If the confidence is low, the ticket is routed to a human instead of being auto-classified, which reduces misrouted tickets without adding any AI cost.

For support teams with predictable ticket types, this approach is fast, transparent, and much cheaper than calling an LLM on every request. AI can always be added later as a fallback for tickets with low confidence rather than replacing the keyword classifier entirely.

1 Like

Nice writeup, especially the scoring-by-count detail for overlapping categories. Two additions that helped us with a similar setup: 1) Normalize text first (lowercase, strip punctuation) before matching - otherwise “Refund?” or “REFUND” silently miss. 2) For tickets where maxScore is 0 or tied between two categories, route to a cheap LLM classification call instead of defaulting to general - pure keyword matching misses paraphrased requests (e.g. “money back” vs “refund”), and gating the LLM call behind a low-confidence check keeps cost near zero since most tickets still resolve via keywords. Would be curious how your false-negative rate looks on messages that don’t use the expected vocabulary.

1 Like