---
title: "Auto-Tag and Route Support Tickets with n8n and AI"
description: "Build an n8n workflow that classifies and routes support tickets with AI — topic, sentiment, urgency — powered by included credits. No API key needed."
canonical: https://agentroost.app/en/blog/auto-tag-route-support-tickets-n8n
date: 2026-06-12T04:00:00Z
---

[Canonical URL](https://agentroost.app/en/blog/auto-tag-route-support-tickets-n8n)

Every support inbox eventually hits the same wall: someone has to open each ticket, decide what it's about, judge how urgent it is, and drop it into the right queue. That someone is usually doing it for the hundredth time that day.

n8n can handle all of that with a dozen nodes. This post walks through a complete workflow: receive a new ticket → classify topic, urgency, and sentiment with an AI node → branch to the right team → notify. The whole thing runs unattended.

---

## What the workflow does, end to end

1. A **Webhook** or **Schedule Trigger** fires when a new ticket arrives (from Intercom, Zendesk, a contact form, or a plain email).
2. An **AI/LLM node** reads the ticket body and returns structured JSON: topic, priority, and sentiment.
3. A **Switch** node reads the JSON and branches to the correct downstream path (billing, technical, refund, general).
4. Each branch does something: assigns in your helpdesk via HTTP, posts to a Slack channel, sends an email — whatever fits your stack.

---

## Step 1 — Trigger: catch the incoming ticket

For a webhook-based helpdesk, use the **Webhook** node.

```
Node: Webhook
HTTP Method: POST
Path: /ticket-intake
Response Mode: Immediately
```

If you're polling a mailbox or a helpdesk API on a schedule, swap in the **Schedule Trigger** node set to your preferred interval (e.g. every 5 minutes), then follow it with an **HTTP Request** node that calls your helpdesk's list-tickets endpoint with a `since` timestamp stored in a **Static Data** node.

For the rest of this walkthrough, assume the ticket arrives as a JSON body with at least:

```json
{
  "ticket_id": "TKT-1042",
  "subject": "Charged twice this month",
  "body": "Hi, I see two charges of $19.99 on my card for June. Can you fix this?"
}
```

---

## Step 2 — Classify with the AI/LLM node

This is the core step. Add an **AI/LLM** node (in n8n it appears as **Basic LLM Chain** or **AI Agent** depending on your version).

**System prompt:**

```
You are a support ticket classifier. Read the ticket below and return ONLY valid JSON — no prose, no markdown fences.

Schema:
{
  "topic": "billing" | "technical" | "refund" | "feature_request" | "other",
  "priority": "urgent" | "normal" | "low",
  "sentiment": "angry" | "neutral" | "positive"
}

Rules:
- "urgent" = the user mentions data loss, account locked, service down, or double charge.
- "billing" = anything about invoices, charges, subscriptions.
- "refund" = explicitly asks for money back.
- Default to "normal" priority and "neutral" sentiment when uncertain.
```

**User message (expression):**

```
Subject: {{ $json.subject }}

{{ $json.body }}
```

Set **Output Parser** to **JSON** (or use a **Set** node after to parse the string). The node returns:

```json
{
  "topic": "billing",
  "priority": "urgent",
  "sentiment": "angry"
}
```

> **Why strict JSON?** Free-text AI responses make downstream Switch nodes brittle. Forcing a schema means you can route deterministically on `{{ $json.topic }}` without string-matching hacks.

Model choice matters for cost and speed here. A fast, cheap model (Mistral 7B, GPT-4o Mini, Claude Haiku) handles classification well and keeps per-ticket cost negligible.

---

## Step 3 — Route with a Switch node

Add a **Switch** node immediately after the AI output.

| Rule | Condition | Output branch |
|---|---|---|
| 1 | `{{ $json.topic }}` equals `billing` | → Billing team |
| 2 | `{{ $json.topic }}` equals `refund` | → Finance team |
| 3 | `{{ $json.topic }}` equals `technical` | → Tech team |
| 4 | `{{ $json.priority }}` equals `urgent` | → Escalation queue |
| Default | — | → General queue |

Put the `urgent` rule above the topic rules if you want urgency to override everything else. Otherwise topic takes precedence and urgency is layered in per-branch.

---

## Step 4 — Act on each branch

Each branch gets its own sub-chain. A few common patterns:

**Assign in your helpdesk (HTTP Request):**
```
Method: PATCH
URL: https://api.zendesk.com/v2/tickets/{{ $json.ticket_id }}.json
Body:
{
  "ticket": {
    "tags": ["{{ $json.topic }}", "{{ $json.priority }}"],
    "group_id": 12345
  }
}
```

**Post to Slack:**
```
Node: Slack
Resource: Message
Channel: #support-billing
Text: :rotating_light: *Urgent billing ticket* TKT-{{ $json.ticket_id }}
      Sentiment: {{ $json.sentiment }}
      > {{ $json.subject }}
```

**Send an internal email (Gmail / SMTP node):**
Use the **Send Email** node with a subject like `[{{ $json.priority | upper }}] {{ $json.subject }}` and include the full ticket body in the HTML body field.

---

## Step 5 — Write back the tags (optional but useful)

Before the branches diverge, add a single **HTTP Request** node that writes the AI-generated tags back to your helpdesk or database. This creates an audit trail — you can later filter tickets by the tag and measure classification accuracy.

```json
{
  "ticket_id": "{{ $json.ticket_id }}",
  "ai_topic": "{{ $json.topic }}",
  "ai_priority": "{{ $json.priority }}",
  "ai_sentiment": "{{ $json.sentiment }}"
}
```

---

## Pitfalls to avoid

- **Non-deterministic JSON.** Some models occasionally wrap JSON in markdown code fences (` ```json `) despite instructions. Add a **Code** node after the AI step to strip fences before parsing: `return [{ json: JSON.parse(items[0].json.text.replace(/```json|```/g, '').trim()) }]`.
- **Missing urgency escalation.** If you only branch on topic, a billing question from an angry, locked-out user lands in a normal queue. Combine `topic + priority` in your routing logic.
- **Webhook timeouts.** Some helpdesks re-send if they don't get a 200 quickly. Set the Webhook node to **Respond Immediately** and process asynchronously — n8n handles this natively.
- **Volume spikes.** Classification is cheap per ticket, but a sudden flood can queue up. Consider a **Wait** node or throttle in the Schedule Trigger path.

---

## Run it on AgentRoost — your own n8n instance, no DevOps

Here's how this looks on AgentRoost:

1. [Sign up](/en/pricing) — email, Google, Microsoft, or Discord.
2. Pick the **n8n** framework, name your instance.
3. Your private n8n editor opens at `https://<your-id>.agentroost.app` — it's your instance, your login, your data.
4. Build the workflow above. When you drop in the AI/LLM node, it's already connected to included credits. No OpenAI key to create, no billing dashboard to configure, no BYOK.
5. Activate the workflow. Webhooks get a public HTTPS URL instantly.

The classifier runs on those included credits. Every competing option — n8n Cloud, Elestio, Sliplane, a self-hosted VPS — requires you to wire in your own API key and absorb the per-token cost separately. On AgentRoost it's one flat price starting at $19.99/mo, cancel anytime, 14-day money-back.

[Get started with n8n on AgentRoost →](/en/agents/n8n)

---

## What to build next

Once the basic classifier is running, a few natural extensions:

- **Sentiment escalation loop**: if `sentiment === "angry"` AND the ticket is more than 2 hours old with no reply, trigger a follow-up ping to the assignee via Slack.
- **Auto-draft a reply**: after classification, pass the ticket to a second AI node that drafts a first response. A human reviews and sends — saves 2-3 minutes per ticket.
- **Weekly digest**: a Schedule Trigger every Monday that queries your database for the past week's tag distribution and emails a summary. Spot recurring pain points before they become a trend.

The routing workflow is the foundation. Everything else layers on top.
