Implementing Intelligent Support Ticket Routing: Matching Tickets to Experts
Hoshang Mehta
Implementing Intelligent Support Ticket Routing: Matching Tickets to Experts
Tickets are routed manually or by simple rules. Complex tickets go to the wrong team. Specialists are underutilized. Routing delays resolution. Teams can't track routing accuracy or optimize assignments. The result: tickets bounce between teams, resolution time increases, and customer satisfaction drops.
The problem: A billing question arrives. It gets routed to technical support by mistake. The technical support agent spends 10 minutes trying to help, realizes it's a billing issue, reassigns it. The billing team picks it up 20 minutes later. By the time the customer gets an answer, 45 minutes have passed. The customer is frustrated. The support team is inefficient.
The solution: An intelligent routing system that analyzes ticket content, classifies intent, maps agent expertise, and matches tickets to the right agent or team—all automatically. This post walks through building a repeatable architecture that transforms manual routing into intelligent assignment.
The Problem with Manual Ticket Routing
Manual routing breaks down at scale. Here's what happens:
The Wrong Team Problem
Tickets get routed incorrectly:
- Billing questions go to technical support
- Bug reports go to sales
- Feature requests go to support
- Account access issues go to billing
The math: If 30% of tickets require reassignment and each reassignment adds 15 minutes of delay, that's 4.5 hours of wasted time per 100 tickets.
The Workload Imbalance Problem
Some agents are overloaded while others are idle:
- Agent A has 15 open tickets
- Agent B has 2 open tickets
- New tickets still go to Agent A because of round-robin routing
The impact: Agent A's response time increases, customers wait longer, agent burnout increases.
The Expertise Mismatch Problem
Generalists handle specialized issues:
- A complex API integration issue goes to a general support agent
- The agent doesn't have the expertise, escalates to engineering
- Resolution time increases by 2-3 hours
The cost: Specialized agents are underutilized. Complex issues take longer to resolve.
The Context Loss Problem
Routing decisions don't consider customer context:
- An enterprise customer's critical issue gets routed to a junior agent
- A high-value customer's billing question goes to general support
- Customer tier and value aren't factored into routing
The missed opportunity: High-value customers should get priority routing to experienced agents, but manual routing treats everyone the same.
An Intelligent Routing Architecture
The solution is a routing system that analyzes tickets, classifies intent, maps expertise, and matches tickets to optimal agents.
Architecture Components
1. Intent Classification Engine
- Analyzes ticket content (subject, description)
- Classifies intent (billing, technical, feature request, etc.)
- Extracts key topics and entities
- Scores classification confidence
2. Customer Context Integration
- Retrieves customer tier (enterprise, mid-market, self-serve)
- Checks support history (previous tickets, satisfaction)
- Identifies special handling requirements
- Factors customer value into routing
3. Expertise Mapping System
- Tracks agent skills and specializations
- Maps topics to agent expertise
- Scores agent proficiency in each area
- Tracks historical performance by topic
4. Matching Algorithm
- Combines intent, context, and expertise
- Scores potential agent matches
- Considers workload and availability
- Applies business rules (enterprise → dedicated team)
5. Performance Tracking
- Monitors routing accuracy
- Tracks resolution times by route
- Collects agent feedback
- Optimizes routing rules over time
Ticket Classification and Intent Detection
The first step is classifying what the ticket is about. Use natural language processing to extract intent from ticket content.
Intent Classification Model
Classify tickets into categories:
CREATE MATERIALIZED VIEW ticket_intent_classification AS
SELECT
t.ticket_id,
t.subject,
t.description,
t.customer_email,
t.created_at,
-- Intent Classification
CASE
-- Billing Intent
WHEN LOWER(t.subject || ' ' || t.description) LIKE '%billing%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%payment%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%invoice%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%charge%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%refund%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%subscription%' THEN 'billing'
-- Technical Intent
WHEN LOWER(t.subject || ' ' || t.description) LIKE '%bug%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%error%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%broken%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%not working%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%crash%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%api%' THEN 'technical'
-- Account Access Intent
WHEN LOWER(t.subject || ' ' || t.description) LIKE '%account%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%login%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%access%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%password%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%locked%' THEN 'account_access'
-- Feature Request Intent
WHEN LOWER(t.subject || ' ' || t.description) LIKE '%feature%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%request%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%suggestion%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%enhancement%' THEN 'feature_request'
-- How-to Intent
WHEN LOWER(t.subject || ' ' || t.description) LIKE '%how to%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%question%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%help%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%guide%' THEN 'how_to'
-- Integration Intent
WHEN LOWER(t.subject || ' ' || t.description) LIKE '%integration%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%webhook%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%sso%'
OR LOWER(t.subject || ' ' || t.description) LIKE '%oauth%' THEN 'integration'
-- Default
ELSE 'general'
END as intent_category,
-- Intent Confidence (based on keyword matches)
CASE
WHEN (
(LOWER(t.subject || ' ' || t.description) LIKE '%billing%' OR LOWER(t.subject || ' ' || t.description) LIKE '%payment%')::int +
(LOWER(t.subject || ' ' || t.description) LIKE '%bug%' OR LOWER(t.subject || ' ' || t.description) LIKE '%error%')::int +
(LOWER(t.subject || ' ' || t.description) LIKE '%account%' OR LOWER(t.subject || ' ' || t.description) LIKE '%login%')::int
) >= 2 THEN 0.9
WHEN (
(LOWER(t.subject || ' ' || t.description) LIKE '%billing%' OR LOWER(t.subject || ' ' || t.description) LIKE '%payment%')::int +
(LOWER(t.subject || ' ' || t.description) LIKE '%bug%' OR LOWER(t.subject || ' ' || t.description) LIKE '%error%')::int +
(LOWER(t.subject || ' ' || t.description) LIKE '%account%' OR LOWER(t.subject || ' ' || t.description) LIKE '%login%')::int
) = 1 THEN 0.7
ELSE 0.5
END as intent_confidence
FROM tickets t
WHERE t.status = 'new';
Advanced Intent Classification with LLMs
For more accurate classification, use LLMs:
def classify_ticket_intent(ticket_subject: str, ticket_description: str) -> dict:
prompt = f"""Classify this support ticket into one of these categories:
- billing: Payment, invoices, subscriptions, refunds
- technical: Bugs, errors, broken features, API issues
- account_access: Login, password, account access issues
- feature_request: Feature suggestions, enhancements
- how_to: Questions about how to use features
- integration: API integrations, webhooks, SSO, OAuth
- general: Other questions
Ticket Subject: {ticket_subject}
Ticket Description: {ticket_description}
Return JSON with 'category' and 'confidence' (0-1)."""
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"}
)
result = json.loads(response.choices[0].message.content)
return {
"intent_category": result["category"],
"intent_confidence": result["confidence"]
}
Agent Expertise Mapping
Map agent skills and specializations to ticket intents. Track which agents excel at which types of tickets.
Agent Expertise Schema
-- Agent expertise table
CREATE TABLE agent_expertise (
agent_id VARCHAR(255) PRIMARY KEY,
agent_name VARCHAR(255),
team VARCHAR(100),
expertise_areas JSONB, -- {'billing': 0.9, 'technical': 0.7, ...}
specialization_tags TEXT[], -- ['api', 'integrations', 'enterprise']
avg_resolution_time_hours DECIMAL,
satisfaction_score_avg DECIMAL,
tickets_resolved INTEGER,
is_available BOOLEAN DEFAULT true,
current_workload INTEGER DEFAULT 0,
max_workload INTEGER DEFAULT 10
);
-- Example agent data
INSERT INTO agent_expertise VALUES
('agent-001', 'Sarah Chen', 'billing_support',
'{"billing": 0.95, "account_access": 0.8, "technical": 0.3}'::jsonb,
ARRAY['billing', 'subscriptions'],
2.5, 4.8, 450, true, 3, 10),
('agent-002', 'Mike Johnson', 'technical_support',
'{"technical": 0.9, "integration": 0.85, "billing": 0.2}'::jsonb,
ARRAY['api', 'integrations', 'bugs'],
3.2, 4.6, 380, true, 5, 10),
('agent-003', 'Emily Davis', 'enterprise_support',
'{"technical": 0.8, "integration": 0.9, "billing": 0.7}'::jsonb,
ARRAY['enterprise', 'api', 'integrations'],
4.1, 4.9, 220, true, 2, 8);
Expertise Scoring
Calculate expertise scores based on historical performance:
-- Calculate agent expertise from historical performance
CREATE OR REPLACE FUNCTION calculate_agent_expertise()
RETURNS void AS $$
BEGIN
UPDATE agent_expertise ae
SET expertise_areas = (
SELECT jsonb_object_agg(
intent_category,
CASE
WHEN avg_resolution_time < 2 THEN 0.95
WHEN avg_resolution_time < 4 THEN 0.85
WHEN avg_resolution_time < 6 THEN 0.75
ELSE 0.65
END *
CASE
WHEN avg_satisfaction > 4.5 THEN 1.0
WHEN avg_satisfaction > 4.0 THEN 0.9
WHEN avg_satisfaction > 3.5 THEN 0.8
ELSE 0.7
END
)
FROM (
SELECT
tic.intent_category,
AVG(EXTRACT(EPOCH FROM (t.resolved_at - t.created_at)) / 3600) as avg_resolution_time,
AVG(t.satisfaction_score) as avg_satisfaction
FROM support_tickets t
JOIN ticket_intent_classification tic ON t.ticket_id = tic.ticket_id
WHERE t.assigned_agent_id = ae.agent_id
AND t.status = 'resolved'
AND t.resolved_at > CURRENT_TIMESTAMP - INTERVAL '90 days'
GROUP BY tic.intent_category
) performance
)
WHERE EXISTS (
SELECT 1 FROM support_tickets
WHERE assigned_agent_id = ae.agent_id
AND resolved_at > CURRENT_TIMESTAMP - INTERVAL '90 days'
);
END;
$$ LANGUAGE plpgsql;
Matching Algorithm and Assignment
Combine intent classification, customer context, and agent expertise to match tickets to optimal agents.
Matching Score Calculation
CREATE MATERIALIZED VIEW ticket_agent_matches AS
SELECT
t.ticket_id,
tic.intent_category,
tic.intent_confidence,
cc.customer_tier,
tps.priority_category,
-- Calculate match score for each agent
jsonb_object_agg(
ae.agent_id,
(
-- Expertise match (0-40 points)
COALESCE((ae.expertise_areas->>tic.intent_category)::float, 0.0) * 40 +
-- Availability bonus (0-20 points)
CASE WHEN ae.is_available AND ae.current_workload < ae.max_workload THEN 20 ELSE 0 END +
-- Workload balance (0-20 points)
CASE
WHEN ae.current_workload = 0 THEN 20
WHEN ae.current_workload < ae.max_workload * 0.5 THEN 15
WHEN ae.current_workload < ae.max_workload * 0.8 THEN 10
ELSE 5
END +
-- Customer tier match (0-20 points)
CASE
WHEN cc.customer_tier = 'enterprise' AND ae.team = 'enterprise_support' THEN 20
WHEN cc.customer_tier != 'enterprise' AND ae.team != 'enterprise_support' THEN 15
ELSE 10
END
)
) as agent_match_scores
FROM tickets t
JOIN ticket_intent_classification tic ON t.ticket_id = tic.ticket_id
LEFT JOIN customer_context_comprehensive cc ON t.customer_email = cc.email
LEFT JOIN ticket_priority_scores tps ON t.ticket_id = tps.ticket_id
CROSS JOIN agent_expertise ae
WHERE t.status = 'new'
AND ae.is_available = true
GROUP BY t.ticket_id, tic.intent_category, tic.intent_confidence, cc.customer_tier, tps.priority_category;
Assignment Logic
CREATE MATERIALIZED VIEW ticket_assignments AS
SELECT
tam.ticket_id,
tam.intent_category,
tam.customer_tier,
-- Find best matching agent
(
SELECT agent_id
FROM jsonb_each_text(tam.agent_match_scores) scores
JOIN agent_expertise ae ON scores.key = ae.agent_id
WHERE ae.is_available = true
AND ae.current_workload < ae.max_workload
ORDER BY scores.value::float DESC
LIMIT 1
) as assigned_agent_id,
-- Best match score
(
SELECT MAX(scores.value::float)
FROM jsonb_each_text(tam.agent_match_scores) scores
JOIN agent_expertise ae ON scores.key = ae.agent_id
WHERE ae.is_available = true
AND ae.current_workload < ae.max_workload
) as match_score,
-- Assigned team (from agent)
(
SELECT team
FROM agent_expertise
WHERE agent_id = (
SELECT agent_id
FROM jsonb_each_text(tam.agent_match_scores) scores
JOIN agent_expertise ae ON scores.key = ae.agent_id
WHERE ae.is_available = true
AND ae.current_workload < ae.max_workload
ORDER BY scores.value::float DESC
LIMIT 1
)
) as assigned_team
FROM ticket_agent_matches tam;
Routing Decision Flow
Routing Performance Analytics
Track routing accuracy and optimize assignments over time. Monitor which routes work and which don't.
Performance Metrics
-- Routing performance view
CREATE MATERIALIZED VIEW routing_performance AS
SELECT
ta.assigned_team,
ta.assigned_agent_id,
tic.intent_category,
COUNT(*) as tickets_routed,
COUNT(*) FILTER (WHERE t.status = 'resolved' AND t.resolved_at - t.created_at < INTERVAL '4 hours') as resolved_within_sla,
AVG(EXTRACT(EPOCH FROM (t.resolved_at - t.created_at)) / 3600) as avg_resolution_time_hours,
AVG(t.satisfaction_score) as avg_satisfaction,
COUNT(*) FILTER (WHERE t.reassigned = true) as reassignments,
COUNT(*) FILTER (WHERE t.reassigned = true)::float / COUNT(*) as reassignment_rate
FROM ticket_assignments ta
JOIN tickets t ON ta.ticket_id = t.ticket_id
JOIN ticket_intent_classification tic ON t.ticket_id = tic.ticket_id
WHERE t.created_at > CURRENT_TIMESTAMP - INTERVAL '30 days'
GROUP BY ta.assigned_team, ta.assigned_agent_id, tic.intent_category;
Feedback Loop
-- Track routing feedback
CREATE TABLE routing_feedback (
feedback_id UUID PRIMARY KEY,
ticket_id UUID,
assigned_agent_id VARCHAR(255),
was_correct_route BOOLEAN,
should_have_been_routed_to VARCHAR(255),
feedback_notes TEXT,
created_at TIMESTAMP
);
-- Update routing rules based on feedback
CREATE OR REPLACE FUNCTION optimize_routing_rules()
RETURNS void AS $$
BEGIN
-- Identify patterns in incorrect routings
WITH incorrect_routes AS (
SELECT
tic.intent_category,
rf.should_have_been_routed_to,
COUNT(*) as incorrect_count
FROM routing_feedback rf
JOIN tickets t ON rf.ticket_id = t.ticket_id
JOIN ticket_intent_classification tic ON t.ticket_id = tic.ticket_id
WHERE rf.was_correct_route = false
AND rf.created_at > CURRENT_TIMESTAMP - INTERVAL '30 days'
GROUP BY tic.intent_category, rf.should_have_been_routed_to
)
-- Update routing rules (simplified - implement actual rule updates)
SELECT * FROM incorrect_routes;
END;
$$ LANGUAGE plpgsql;
Schema Overview
The routing architecture uses a hierarchical schema that traces from ticket classification to agent assignment.
Key relationships:
tickets→ticket_intent_classification(classifies intent)ticket_intent_classification+agent_expertise→ticket_agent_matches(calculates matches)ticket_agent_matches→ticket_assignments(selects best match)ticket_assignments→routing_performance(tracks results)
Traceability: Every routing decision can be traced back to intent classification, expertise matching, and assignment logic.
Closing Thoughts
Building an intelligent routing system transforms ticket assignment from guesswork into data-driven decisions. The key is treating routing as a matching problem—not a manual process.
Key takeaways:
-
Intent classification is foundational: Accurately classify what tickets are about. This determines which agents can handle them.
-
Expertise mapping enables matching: Track agent skills and specializations. Match tickets to agents who excel at those topics.
-
Context matters: Consider customer tier, priority, and special requirements. Enterprise customers need different routing than self-serve.
-
Workload balancing prevents overload: Distribute tickets evenly. Don't overload some agents while others are idle.
-
Performance tracking enables optimization: Monitor routing accuracy, resolution times, satisfaction. Use feedback to improve routing rules.
The architecture described here is repeatable. Start with simple intent classification and basic routing rules, then add expertise mapping and matching algorithms. Iterate based on performance data.
Most importantly: this isn't about replacing human judgment—it's about augmenting it. Agents still make final decisions. The routing system provides intelligent suggestions, reducing manual work and improving accuracy.
This architecture pattern is implemented in Pylar, but the concepts apply to any system routing tickets. The key is the classification → expertise mapping → matching → assignment pipeline.