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)

Overview

This workflow uses the Zendesk Trigger node to receive new ticket events, a Code node to run keyword matching against a topic map, and an HTTP Request node to update the ticket with tags and a group assignment via the Zendesk API. Because n8n sits outside Zendesk, you can extend this workflow to also log to a spreadsheet, notify Slack, or call other APIs as part of the same triage step.

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

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.

Need help implementing this?

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