Auto-triage and tag Zendesk tickets using n8n

low complexityCost: $0-24/mo

Prerequisites

Prerequisites
  • n8n instance (cloud or self-hosted)
  • Zendesk API credentials: account email and API token (Admin Center → Apps and integrations → APIs → Zendesk API)
  • Your Zendesk subdomain (the your-company part of your-company.zendesk.com)

Why n8n?

n8n is the best option when you want to extend triage beyond just tagging — log to a spreadsheet, notify Slack, call another API, or chain into a second workflow all in the same trigger. The visual builder makes the keyword matching logic transparent, and you can test with real ticket data before activating.

The main advantage over native triggers is extensibility. The trade-off is a small latency penalty — the Zendesk Trigger node uses webhooks so it's near-instant, but there's a few seconds of processing time. Self-hosted n8n is free with unlimited executions; Cloud starts at $24/mo.

How it works

  • Zendesk Trigger node receives new ticket events via webhook (automatically configured by n8n)
  • Code node runs keyword matching against a topic-keyword map and returns matched tags + target group
  • HTTP Request looks up group IDs from the Zendesk Groups API
  • HTTP Request updates the ticket via PUT /tickets/{id}.json with the topic tags and group assignment

Step 1: Set up Zendesk credentials in n8n

Create a new credential in n8n:

  • Type: Zendesk API
  • Subdomain: your Zendesk subdomain (e.g., acme)
  • Email: your Zendesk account email
  • API Token: your Zendesk API token

Zendesk API auth uses the format {'{'}email{'}'}/token:{'{'}api_token{'}'} as the username with Basic auth. n8n handles this automatically when you use the Zendesk credential type.

Step 2: Add a Zendesk Trigger node

Add a Zendesk Trigger node as the workflow trigger:

  • Credential: Select the Zendesk credential you just created
  • Event: Ticket Created

This node creates a webhook in your Zendesk instance automatically. When a new ticket arrives, Zendesk sends the ticket data to n8n.

What the trigger sends

The Zendesk Trigger node delivers the full ticket object including id, subject, description, tags, status, group_id, and requester details. You'll use subject and description for keyword matching.

Step 3: Add a Code node for keyword matching

Add a Code node connected to the trigger. This node checks the ticket text against a topic-keyword map and returns the matched tags and target group:

const ticket = $input.first().json;
const subject = (ticket.subject || '').toLowerCase();
const description = (ticket.description || '').toLowerCase();
const text = `${subject} ${description}`;
 
const topicMap = {
  'topic-billing': ['invoice', 'charge', 'billing', 'payment', 'receipt', 'subscription', 'refund'],
  'topic-shipping': ['tracking', 'delivery', 'shipping', 'package', 'lost', 'delayed', 'transit'],
  'topic-product': ['broken', 'defective', 'quality', 'size', 'color', 'damaged', 'wrong item'],
  'topic-account': ['password', 'login', 'account', 'access', 'locked', 'sign in', 'reset'],
  'topic-technical': ['error', 'bug', 'crash', 'not loading', 'api', 'integration', 'timeout'],
};
 
const groupMap = {
  'topic-billing': 'Billing Support',
  'topic-shipping': 'Shipping Support',
  'topic-product': 'Product Support',
  'topic-account': 'Account Support',
  'topic-technical': 'Technical Support',
};
 
const matchedTopics = [];
for (const [tag, keywords] of Object.entries(topicMap)) {
  if (keywords.some(kw => text.includes(kw))) {
    matchedTopics.push(tag);
  }
}
 
const primaryTopic = matchedTopics[0] || 'topic-uncategorized';
 
return [{
  json: {
    ticketId: ticket.id,
    tags: matchedTopics.length > 0 ? matchedTopics : ['topic-uncategorized'],
    group: groupMap[primaryTopic] || 'General Support',
    matchedTopics,
  }
}];

The Code node outputs the ticket ID, an array of matched topic tags, and the target group name.

Keyword order determines the primary group

When a ticket matches multiple topics, the first match in topicMap becomes the primary group assignment. Order your topics from most specific (Technical) to least specific (Shipping) if you want specificity to win ties.

Step 4: Look up group IDs

The Zendesk API requires a numeric group_id, not a group name. Add an HTTP Request node to fetch your groups:

  • Method: GET
  • URL: https://{'{'}your-subdomain{'}'}.zendesk.com/api/v2/groups.json
  • Authentication: Predefined Credential Type → Zendesk API
  • Credential: Select your Zendesk credential

Then add another Code node to map the group name to the ID:

const groups = $('HTTP Request').first().json.groups;
const targetGroup = $('Code').first().json.group;
const classification = $('Code').first().json;
 
const groupId = groups.find(g => g.name === targetGroup)?.id;
 
return [{
  json: {
    ...classification,
    groupId: groupId || null,
  }
}];
Cache group IDs to save API calls

Group IDs rarely change. After you look them up once, you can hardcode them in the Code node to skip the extra API call. Use the format: const groupIds = {'{'} 'Billing Support': 12345, 'Shipping Support': 67890 {'}'}.

Step 5: Update the ticket via Zendesk API

Add a final HTTP Request node to apply the tags and group assignment:

  • Method: PUT
  • URL: https://{'{'}your-subdomain{'}'}.zendesk.com/api/v2/tickets/{'{'}$json.ticketId{'}'}.json
  • Authentication: Predefined Credential Type → Zendesk API
  • Credential: Select your Zendesk credential
  • Body Content Type: JSON
  • Body:
{
  "ticket": {
    "tags": "{{ $json.tags }}",
    "group_id": "{{ $json.groupId }}"
  }
}
Tags are additive by default

The Zendesk API's tags field on PUT replaces the entire tag array. Since you're processing newly created tickets, the tag list is typically empty. If you need to preserve existing tags, fetch the current ticket first and merge the arrays in a Code node before updating.

Step 6: Add error handling and activate

  1. Enable Retry On Fail on the HTTP Request nodes (2 retries, 5-second wait)
  2. Add an Error Workflow to alert you via email or Slack if the workflow fails
  3. Run a test by creating a ticket in Zendesk with subject "Question about my invoice"
  4. Verify the topic-billing tag appears and the ticket is assigned to Billing Support
  5. Toggle the workflow to Active

Troubleshooting

Common questions

Why use n8n instead of native Zendesk triggers for keyword matching?

If all you need is tagging and routing, native triggers are simpler. Use n8n when you want to chain triage with other actions in the same workflow — Slack notifications, spreadsheet logging, calling external APIs, or running more complex matching logic than Zendesk conditions allow.

Can I use AI classification instead of keyword matching in n8n?

Yes. Replace the Code node with an HTTP Request node calling the Anthropic API (or OpenAI) to classify the ticket text. This adds ~1-2 seconds of latency and ~$0.001 per ticket in API costs, but handles vague and multilingual tickets that keyword matching misses.

What happens if the webhook fails?

n8n retries failed webhook deliveries automatically. Enable "Retry On Fail" on HTTP Request nodes for resilience against temporary Zendesk API errors. If n8n is down, Zendesk will retry the webhook delivery for up to 48 hours.

Cost

  • n8n Cloud: Each ticket triggers ~4 node executions. 500 tickets/month = ~2,000 executions. Fits within the Starter plan.
  • Self-hosted: Free. You provide the infrastructure.

Looking to scale your AI operations?

We build and optimize automation systems for mid-market businesses. Let's discuss the right approach for your team.