Automated customer support improves response times and agent efficiency. This guide covers building intelligent support workflows with n8n for ticket routing, AI responses, and multi-channel management.
Intelligent Ticket Routing
Build smart ticket classification and routing:
// Multi-Channel Ticket Ingestion Workflow
// 1. Email Ticket Trigger
const emailTrigger = {
name: "Email Ticket Trigger",
type: "n8n-nodes-base.imapEmail",
parameters: {
mailbox: "INBOX",
action: "read",
options: {
markSeen: true,
forceReconnect: true
}
}
};
// 2. Ticket Classification Function
function classifyTicket(items) {
const ticket = items[0].json;
const subject = (ticket.subject || '').toLowerCase();
const body = (ticket.text || ticket.html || '').toLowerCase();
const content = subject + ' ' + body;
// Category classification
const categories = {
billing: ['invoice', 'payment', 'charge', 'refund', 'subscription', 'pricing', 'bill'],
technical: ['error', 'bug', 'crash', 'not working', 'broken', 'issue', 'problem', 'failed'],
account: ['password', 'login', 'access', 'account', 'profile', 'settings', 'security'],
sales: ['demo', 'pricing', 'enterprise', 'quote', 'trial', 'upgrade', 'plan'],
general: ['question', 'help', 'information', 'inquiry', 'support']
};
let detectedCategory = 'general';
let maxScore = 0;
for (const [category, keywords] of Object.entries(categories)) {
const score = keywords.filter(kw => content.includes(kw)).length;
if (score > maxScore) {
maxScore = score;
detectedCategory = category;
}
}
// Priority detection
const urgentKeywords = ['urgent', 'asap', 'immediately', 'critical', 'emergency', 'down', 'outage'];
const highKeywords = ['important', 'blocking', 'cannot', 'broken', 'failed'];
let priority = 'normal';
if (urgentKeywords.some(kw => content.includes(kw))) {
priority = 'urgent';
} else if (highKeywords.some(kw => content.includes(kw))) {
priority = 'high';
}
// Sentiment analysis (simple)
const negativeWords = ['frustrated', 'angry', 'disappointed', 'terrible', 'worst', 'hate', 'unacceptable'];
const sentiment = negativeWords.some(w => content.includes(w)) ? 'negative' : 'neutral';
return [{
json: {
ticket_id: `TKT-${Date.now()}`,
source: 'email',
from: ticket.from,
subject: ticket.subject,
body: ticket.text || ticket.html,
received_at: new Date().toISOString(),
category: detectedCategory,
priority: priority,
sentiment: sentiment,
confidence: Math.min(maxScore / 3, 1),
raw_email: ticket
}
}];
}
// 3. Route to Appropriate Queue
const routeTicket = {
name: "Route Ticket",
type: "n8n-nodes-base.switch",
parameters: {
rules: {
rules: [
{
value1: "={{$json.priority}}",
operation: "equals",
value2: "urgent"
},
{
value1: "={{$json.category}}",
operation: "equals",
value2: "billing"
},
{
value1: "={{$json.category}}",
operation: "equals",
value2: "technical"
},
{
value1: "={{$json.category}}",
operation: "equals",
value2: "sales"
}
]
}
}
};
// 4. Assign to Agent Based on Skills and Availability
async function assignToAgent(items) {
const ticket = items[0].json;
// Get available agents with matching skills
const agents = await getAvailableAgents(ticket.category);
// Load balancing - find agent with lowest current workload
let selectedAgent = null;
let lowestWorkload = Infinity;
for (const agent of agents) {
const workload = await getAgentWorkload(agent.id);
if (workload < lowestWorkload && workload < agent.max_tickets) {
lowestWorkload = workload;
selectedAgent = agent;
}
}
// If no agent available, add to queue
if (!selectedAgent) {
return [{
json: {
...ticket,
status: 'queued',
assigned_agent: null,
queue_position: await getQueuePosition(ticket.category)
}
}];
}
return [{
json: {
...ticket,
status: 'assigned',
assigned_agent: {
id: selectedAgent.id,
name: selectedAgent.name,
email: selectedAgent.email
},
assigned_at: new Date().toISOString()
}
}];
}
// Helper functions
async function getAvailableAgents(category) {
// Would query agent database
return [
{ id: 'agent1', name: 'Alice', email: 'alice@company.com', skills: ['billing', 'general'], max_tickets: 10 },
{ id: 'agent2', name: 'Bob', email: 'bob@company.com', skills: ['technical', 'general'], max_tickets: 8 }
].filter(a => a.skills.includes(category) || a.skills.includes('general'));
}
async function getAgentWorkload(agentId) {
// Would query current ticket assignments
return Math.floor(Math.random() * 10);
}
async function getQueuePosition(category) {
return Math.floor(Math.random() * 20) + 1;
}AI-Powered Response Generation
Generate intelligent responses with AI:
// AI Response Generation Workflow
// 1. Prepare Context for AI
function prepareAIContext(items) {
const ticket = items[0].json;
// Build context from ticket history and knowledge base
const context = {
ticket_id: ticket.ticket_id,
category: ticket.category,
customer_message: ticket.body,
customer_email: ticket.from,
priority: ticket.priority,
sentiment: ticket.sentiment
};
// System prompt based on category
const systemPrompts = {
billing: `You are a helpful billing support agent. Be empathetic about billing concerns.
Focus on resolving payment issues quickly. Always offer to help with refunds or adjustments when appropriate.`,
technical: `You are a technical support specialist. Ask clarifying questions about the issue.
Provide step-by-step troubleshooting instructions. Escalate complex issues appropriately.`,
account: `You are an account security specialist. Verify identity carefully before making changes.
Guide users through secure password reset procedures.`,
sales: `You are a sales representative. Be enthusiastic but not pushy.
Focus on understanding customer needs and matching them with appropriate solutions.`,
general: `You are a friendly customer support agent. Be helpful and professional.
If you can't answer something, offer to connect the customer with the right team.`
};
context.system_prompt = systemPrompts[ticket.category] || systemPrompts.general;
return [{ json: context }];
}
// 2. Generate AI Response
const generateAIResponse = {
name: "Generate AI Response",
type: "n8n-nodes-base.openAi",
parameters: {
operation: "text",
model: "gpt-4",
messages: [
{
role: "system",
content: `{{$json.system_prompt}}
Your response guidelines:
- Be professional and empathetic
- Keep responses concise but complete
- Use proper grammar and formatting
- Include specific next steps when applicable
- Never share sensitive information
- If unsure, offer to escalate to a human agent`
},
{
role: "user",
content: `Customer email: {{$json.customer_email}}
Category: {{$json.category}}
Priority: {{$json.priority}}
Customer sentiment: {{$json.sentiment}}
Customer message:
{{$json.customer_message}}
Please draft a helpful response.`
}
],
options: {
temperature: 0.7,
maxTokens: 500
}
}
};
// 3. Response Quality Check
function checkResponseQuality(items) {
const aiResponse = items[0].json;
const context = $('Prepare AI Context').first().json;
const responseText = aiResponse.message?.content || aiResponse.text || '';
const qualityChecks = {
hasGreeting: /^(hi|hello|dear|thank)/i.test(responseText),
hasSignoff: /(regards|sincerely|best|thanks)/i.test(responseText),
appropriateLength: responseText.length >= 50 && responseText.length <= 2000,
noSensitiveInfo: !/(password|credit card|ssn|social security)/i.test(responseText),
addressesIssue: responseText.toLowerCase().includes(context.category) ||
responseText.length > 100,
professionalTone: !/(!{2,}|CAPS{3,})/g.test(responseText)
};
const passedChecks = Object.values(qualityChecks).filter(v => v).length;
const qualityScore = passedChecks / Object.keys(qualityChecks).length;
const needsReview = qualityScore < 0.8 ||
context.sentiment === 'negative' ||
context.priority === 'urgent';
return [{
json: {
ticket_id: context.ticket_id,
original_message: context.customer_message,
ai_response: responseText,
quality_checks: qualityChecks,
quality_score: qualityScore,
needs_human_review: needsReview,
auto_send_eligible: qualityScore >= 0.9 && !needsReview
}
}];
}
// 4. Human Review Queue (for flagged responses)
const humanReviewNode = {
name: "Send to Review Queue",
type: "n8n-nodes-base.slack",
parameters: {
operation: "postMessage",
channel: "#support-review",
text: "🔍 *Response Review Needed*",
attachments: [
{
color: "={{$json.quality_score >= 0.8 ? 'warning' : 'danger'}}",
fields: [
{ title: "Ticket ID", value: "={{$json.ticket_id}}", short: true },
{ title: "Quality Score", value: "={{($json.quality_score * 100).toFixed(0)}}%", short: true },
{ title: "Customer Message", value: "={{$json.original_message.substring(0, 200)}}..." },
{ title: "AI Draft Response", value: "={{$json.ai_response.substring(0, 500)}}..." }
],
actions: [
{ type: "button", text: "✅ Approve & Send", value: "approve_{{$json.ticket_id}}" },
{ type: "button", text: "✏️ Edit", value: "edit_{{$json.ticket_id}}" },
{ type: "button", text: "❌ Reject", value: "reject_{{$json.ticket_id}}" }
]
}
]
}
};
// 5. Auto-Send Approved Responses
const autoSendResponse = {
name: "Send Response",
type: "n8n-nodes-base.emailSend",
parameters: {
fromEmail: "support@company.com",
toEmail: "={{$json.customer_email}}",
subject: "Re: {{$json.subject}}",
text: "={{$json.ai_response}}",
options: {
replyTo: "support@company.com"
}
}
};Escalation Workflow
Handle ticket escalation automatically:
// Ticket Escalation Workflow
// 1. Monitor for Escalation Triggers
const escalationTriggers = {
name: "Check Escalation Needed",
type: "n8n-nodes-base.function",
parameters: {
functionCode: `
const ticket = items[0].json;
// Escalation rules
const rules = [
{
name: 'sla_breach',
condition: () => {
const ageHours = (Date.now() - new Date(ticket.created_at).getTime()) / (1000 * 60 * 60);
const slaHours = { urgent: 1, high: 4, normal: 24, low: 48 };
return ageHours > slaHours[ticket.priority] && ticket.status !== 'resolved';
},
escalation_level: 'manager',
reason: 'SLA breach imminent'
},
{
name: 'negative_sentiment_unresolved',
condition: () => ticket.sentiment === 'negative' && ticket.replies > 2 && ticket.status !== 'resolved',
escalation_level: 'senior_agent',
reason: 'Negative sentiment with multiple replies'
},
{
name: 'customer_requested',
condition: () => ticket.body.toLowerCase().includes('speak to manager') ||
ticket.body.toLowerCase().includes('escalate'),
escalation_level: 'manager',
reason: 'Customer requested escalation'
},
{
name: 'vip_customer',
condition: () => ticket.customer_tier === 'enterprise' && ticket.priority !== 'low',
escalation_level: 'senior_agent',
reason: 'VIP customer ticket'
},
{
name: 'technical_complexity',
condition: () => ticket.category === 'technical' &&
(ticket.body.includes('data loss') || ticket.body.includes('security')),
escalation_level: 'engineering',
reason: 'Complex technical issue'
}
];
// Check each rule
const triggeredRules = rules.filter(rule => {
try {
return rule.condition();
} catch {
return false;
}
});
return [{
json: {
...ticket,
escalation_needed: triggeredRules.length > 0,
escalation_rules_triggered: triggeredRules.map(r => ({
name: r.name,
level: r.escalation_level,
reason: r.reason
})),
highest_escalation_level: triggeredRules.length > 0 ?
getHighestLevel(triggeredRules.map(r => r.escalation_level)) : null
}
}];
function getHighestLevel(levels) {
const hierarchy = ['senior_agent', 'manager', 'engineering', 'executive'];
return levels.sort((a, b) => hierarchy.indexOf(b) - hierarchy.indexOf(a))[0];
}
`
}
};
// 2. Execute Escalation
function executeEscalation(items) {
const ticket = items[0].json;
if (!ticket.escalation_needed) {
return [{ json: { ...ticket, escalated: false } }];
}
const escalationActions = {
senior_agent: {
notify: ['senior-support@company.com'],
slack_channel: '#senior-support',
priority_boost: true
},
manager: {
notify: ['support-manager@company.com'],
slack_channel: '#support-managers',
priority_boost: true,
auto_respond: true
},
engineering: {
notify: ['engineering-oncall@company.com'],
slack_channel: '#engineering-support',
create_jira: true
},
executive: {
notify: ['vp-support@company.com', 'cto@company.com'],
slack_channel: '#executive-escalations',
priority_boost: true,
auto_respond: true
}
};
const actions = escalationActions[ticket.highest_escalation_level] || escalationActions.manager;
return [{
json: {
...ticket,
escalated: true,
escalation_level: ticket.highest_escalation_level,
escalation_actions: actions,
escalated_at: new Date().toISOString(),
escalation_reasons: ticket.escalation_rules_triggered.map(r => r.reason)
}
}];
}
// 3. Notify Escalation Recipients
const notifyEscalation = {
name: "Notify via Slack",
type: "n8n-nodes-base.slack",
parameters: {
operation: "postMessage",
channel: "={{$json.escalation_actions.slack_channel}}",
text: "🚨 *Ticket Escalated*",
attachments: [
{
color: "danger",
fields: [
{ title: "Ticket ID", value: "={{$json.ticket_id}}", short: true },
{ title: "Escalation Level", value: "={{$json.escalation_level}}", short: true },
{ title: "Customer", value: "={{$json.customer_email}}", short: true },
{ title: "Priority", value: "={{$json.priority}}", short: true },
{ title: "Reasons", value: "={{$json.escalation_reasons.join('\\n')}}" },
{ title: "Subject", value: "={{$json.subject}}" }
],
actions: [
{ type: "button", text: "View Ticket", url: "={{$env.HELPDESK_URL}}/tickets/{{$json.ticket_id}}" },
{ type: "button", text: "Take Ownership", value: "claim_{{$json.ticket_id}}" }
]
}
]
}
};
// 4. Auto-Response for Escalated Tickets
const escalationAutoResponse = {
name: "Send Escalation Notice to Customer",
type: "n8n-nodes-base.emailSend",
parameters: {
fromEmail: "support@company.com",
toEmail: "={{$json.customer_email}}",
subject: "Re: {{$json.subject}} - Your ticket has been escalated",
text: `Dear Customer,
Thank you for your patience. We understand the importance of your request and have escalated your ticket to our {{$json.escalation_level.replace('_', ' ')}} team for priority handling.
You can expect to hear from a specialist within the next {{$json.priority === 'urgent' ? '30 minutes' : '2 hours'}}.
We apologize for any inconvenience and appreciate your understanding.
Best regards,
Customer Support Team
Ticket Reference: {{$json.ticket_id}}`
}
};Support Analytics Dashboard
Track support metrics:
// Support Analytics Workflow
// 1. Daily Metrics Collection
const dailyMetricsTrigger = {
name: "Daily Metrics Trigger",
type: "n8n-nodes-base.scheduleTrigger",
parameters: {
rule: {
interval: [{ field: "hours", hoursInterval: 24 }]
}
}
};
// 2. Collect Metrics
async function collectSupportMetrics(items) {
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const today = new Date();
// Would query actual helpdesk database
const metrics = {
period: {
start: yesterday.toISOString(),
end: today.toISOString()
},
tickets: {
total_received: 150,
total_resolved: 142,
total_escalated: 8,
by_channel: {
email: 80,
chat: 45,
phone: 20,
social: 5
},
by_category: {
technical: 55,
billing: 35,
account: 25,
sales: 20,
general: 15
},
by_priority: {
urgent: 5,
high: 25,
normal: 100,
low: 20
}
},
response_times: {
first_response_avg_minutes: 15,
first_response_median_minutes: 8,
resolution_avg_hours: 4.5,
resolution_median_hours: 2.1
},
sla: {
first_response_compliance: 0.94,
resolution_compliance: 0.89,
breaches: 12
},
customer_satisfaction: {
csat_score: 4.2,
nps: 45,
surveys_sent: 142,
surveys_completed: 68,
response_rate: 0.48
},
agent_performance: {
tickets_per_agent_avg: 18,
top_performers: [
{ name: 'Alice', tickets_resolved: 28, csat: 4.8 },
{ name: 'Bob', tickets_resolved: 25, csat: 4.5 }
]
},
ai_automation: {
auto_resolved: 35,
auto_resolved_rate: 0.23,
ai_responses_sent: 89,
ai_responses_approved: 82,
approval_rate: 0.92
}
};
return [{ json: metrics }];
}
// 3. Store Metrics
const storeMetrics = {
name: "Store Metrics",
type: "n8n-nodes-base.postgres",
parameters: {
operation: "insert",
table: "support_metrics",
columns: "period_start, period_end, metrics_json, created_at"
}
};
// 4. Generate Report
function generateReport(items) {
const metrics = items[0].json;
const report = {
title: `Daily Support Report - ${new Date().toLocaleDateString()}`,
summary: {
tickets_handled: metrics.tickets.total_received,
resolution_rate: ((metrics.tickets.total_resolved / metrics.tickets.total_received) * 100).toFixed(1) + '%',
avg_response_time: `${metrics.response_times.first_response_avg_minutes} minutes`,
customer_satisfaction: `${metrics.customer_satisfaction.csat_score}/5`
},
highlights: [],
alerts: [],
recommendations: []
};
// Add highlights
if (metrics.sla.first_response_compliance > 0.95) {
report.highlights.push('✅ Excellent first response SLA compliance');
}
if (metrics.ai_automation.auto_resolved_rate > 0.2) {
report.highlights.push(`✅ AI automation resolved ${metrics.ai_automation.auto_resolved} tickets`);
}
// Add alerts
if (metrics.sla.breaches > 10) {
report.alerts.push(`⚠️ ${metrics.sla.breaches} SLA breaches yesterday`);
}
if (metrics.customer_satisfaction.csat_score < 4.0) {
report.alerts.push('⚠️ CSAT score below target');
}
// Add recommendations
if (metrics.tickets.by_category.technical > metrics.tickets.total_received * 0.4) {
report.recommendations.push('Consider creating more technical documentation to reduce ticket volume');
}
return [{ json: report }];
}
// 5. Send Report
const sendReport = {
name: "Send Daily Report",
type: "n8n-nodes-base.slack",
parameters: {
operation: "postMessage",
channel: "#support-metrics",
text: "*{{$json.title}}*",
attachments: [
{
color: "good",
title: "Summary",
fields: [
{ title: "Tickets Handled", value: "={{$json.summary.tickets_handled}}", short: true },
{ title: "Resolution Rate", value: "={{$json.summary.resolution_rate}}", short: true },
{ title: "Avg Response", value: "={{$json.summary.avg_response_time}}", short: true },
{ title: "CSAT", value: "={{$json.summary.customer_satisfaction}}", short: true }
]
}
]
}
};Conclusion
n8n enables powerful customer support automation from ticket routing to AI-powered responses. Build intelligent classification systems, implement escalation workflows, and track comprehensive metrics. Start with basic routing and gradually add AI capabilities and advanced analytics as your support operation scales.