Auto-Tag and Route Support Tickets with n8n and AI
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
- A Webhook or Schedule Trigger fires when a new ticket arrives (from Intercom, Zendesk, a contact form, or a plain email).
- An AI/LLM node reads the ticket body and returns structured JSON: topic, priority, and sentiment.
- A Switch node reads the JSON and branches to the correct downstream path (billing, technical, refund, general).
- 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:
{
"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:
{
"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.
{
"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 + priorityin 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:
- Sign up — email, Google, Microsoft, or Discord.
- Pick the n8n framework, name your instance.
- Your private n8n editor opens at
https://<your-id>.agentroost.app— it's your instance, your login, your data. - 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.
- 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 →
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.
Frequently asked questions
Do I need an OpenAI API key to use the AI node in n8n?
Not on AgentRoost. Your own n8n instance comes with AI/LLM credits already included in the subscription — the AI node is pre-wired and ready to use. Every other hosting option (n8n Cloud, self-hosted, Elestio, Sliplane) requires you to bring your own key and pay for tokens separately.
Which LLM models can the classification node use?
AgentRoost gives you access to 350+ models across providers (OpenAI, Anthropic, Mistral, and more). You can switch the model inside the AI node at any time — useful if you want a cheaper, faster model for high-volume ticket triage.
Can I export my workflows if I ever want to leave?
Yes. Because you own your n8n instance, you can export all workflows as JSON from the n8n editor at any time (Settings → Export all). Your data isn't locked to AgentRoost.
What does AgentRoost cost, and can I cancel?
Plans start at $19.99/mo all-in — that covers the server, the n8n instance, and the included AI credits. Billing is monthly through Polar, cancel anytime, and there's a 14-day money-back guarantee if it isn't a fit.
How does the workflow handle tickets that don't match any category?
Add a final Default branch to your Switch node (or an IF fallback) that routes unrecognized tickets to a catch-all queue or sends a Slack/email alert to a human reviewer. You can also instruct the AI prompt to always return "topic": "other" when uncertain, so the Switch node always has a clean match.