Skip to main content

Support ticket triage

A support inbox receives hundreds of messages a day in several languages: refund requests, bugs, feature questions and the occasional angry customer. Someone reads each one, tags it, picks a priority, looks the customer up and drafts a reply. By the time the queue is sorted, the urgent tickets are already old.

This use case builds the triage step with intellinode: every ticket becomes structured data, urgent tickets are escalated through your own tools, and a reply draft is ready for the agent to approve. The same code runs on OpenAI, Claude or a private model.

A robot assistant sorting support messages into priority lanes Inboxemail, chat, formsany languagechatJsoncategory, prioritysentiment, summaryrunToolslook up the customerescalate, draftHelpdesktagged, routedone function per ticket, any provider

1. Turn every ticket into structured data

A JSON Schema makes the classification reliable: the provider's structured output guarantees the shape, and chatJson returns a parsed object.

const { Chatbot, ChatGPTInput } = require('intellinode');

const triageSchema = {
type: 'object',
properties: {
category: { type: 'string', enum: ['billing', 'bug', 'feature_request', 'account', 'other'] },
priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] },
sentiment: { type: 'string', enum: ['positive', 'neutral', 'negative'] },
language: { type: 'string' },
summary: { type: 'string' },
needsHuman: { type: 'boolean' },
},
required: ['category', 'priority', 'sentiment', 'language', 'summary', 'needsHuman'],
};

const bot = new Chatbot(process.env.OPENAI_API_KEY, 'openai', null, { timeout: 30000, retries: 2 });

async function triage(ticket) {
const input = new ChatGPTInput('You triage customer support tickets for a SaaS company. Answer as JSON.', {
responseSchema: triageSchema,
});
input.addUserMessage(`Subject: ${ticket.subject}\n\n${ticket.body}`);
return bot.chatJson(input);
}

const result = await triage({ subject: 'Charged twice', body: 'I was billed two times this month and nobody answers the phone!' });
// { category: 'billing', priority: 'high', sentiment: 'negative', language: 'en', summary: '...', needsHuman: true }

2. Escalate through your own tools

For tickets that need action, the tool loop lets the model look the customer up, open a case in your helpdesk and draft the reply, all with functions you control. Every tool call is recorded in steps, which is your audit trail.

const tools = [
{
name: 'lookup_customer',
description: 'Find a customer account by email',
parameters: { type: 'object', properties: { email: { type: 'string' } }, required: ['email'] },
handler: async ({ email }) => crm.findByEmail(email), // your CRM client
},
{
name: 'create_case',
description: 'Open a case in the helpdesk and return its id',
parameters: {
type: 'object',
properties: { customerId: { type: 'string' }, priority: { type: 'string' }, summary: { type: 'string' } },
required: ['customerId', 'priority', 'summary'],
},
handler: async (args) => helpdesk.createCase(args),
},
];

async function escalate(ticket, triageResult) {
const input = new ChatGPTInput(
'You are a support operations assistant. Look up the customer, open a case with the triage priority, '
+ 'then write a short, polite reply draft in the customer language.',
);
input.addUserMessage(`Ticket from ${ticket.email}:\n${ticket.body}\n\nTriage: ${JSON.stringify(triageResult)}`);

const { text, steps } = await bot.runTools(input, tools, { maxSteps: 5 });
return { replyDraft: text, actions: steps.map((step) => step.name) };
}

3. Keep private data on your own hardware

Tickets can contain names, addresses and payment details. Route those to a model that runs inside your network, with the same code: only the chatbot changes.

const { OpenAICompatibleInput } = require('intellinode');

const privateBot = new Chatbot(null, 'ollama', null, { model: 'qwen3', timeout: 60000 });

async function triagePrivately(ticket) {
const input = new OpenAICompatibleInput('You triage customer support tickets. Answer as JSON.', { responseSchema: triageSchema });
input.addUserMessage(`Subject: ${ticket.subject}\n\n${ticket.body}`);
return privateBot.chatJson(input);
}

A self-hosted vLLM server works the same way with SupportedChatModels.VLLM and VLLMInput.

4. Fall back when a provider is down

Wrap the call in a fallback chain. Errors keep the HTTP status, so a rate limit and an outage are easy to tell apart.

const { AnthropicInput } = require('intellinode');

const claude = new Chatbot(process.env.ANTHROPIC_API_KEY, 'anthropic', null, { timeout: 30000, retries: 1 });

async function triageWithFallback(ticket) {
try {
return await triage(ticket);
} catch (error) {
if (error.status && error.status < 500 && error.status !== 429) throw error; // a real request problem
const input = new AnthropicInput('You triage customer support tickets. Answer as JSON.', { responseSchema: triageSchema });
input.addUserMessage(`Subject: ${ticket.subject}\n\n${ticket.body}`);
return claude.chatJson(input);
}
}

Why it helps

  • Every ticket is tagged and prioritised in seconds, in the language it arrived in.
  • The tools decide what the model may do; nothing is sent to the customer without an agent's approval.
  • Sensitive tickets never leave your network, and a provider outage does not stop the queue.