🚀

We've launched on Product Hunt

Check us out →
Tutorials

Implementing Intelligent Support Ticket Routing: Matching Tickets to Experts

H

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.

graph TD A[New Ticket] --> B[Intent Classification] A --> C[Customer Context] A --> D[Priority Scoring] B --> E[Expertise Matching] C --> E D --> E E --> F[Team Selection] F --> G[Agent Selection] G --> H[Workload Balancing] H --> I[Assignment] J[Routing Performance] --> K[Feedback Loop] K --> E style B fill:#e1f5ff style E fill:#fff4e1 style G fill:#e8f5e9

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

graph TD A[New Ticket] --> B[Intent Classification] B --> C{Intent Category} C -->|Billing| D[Filter Billing Agents] C -->|Technical| E[Filter Technical Agents] C -->|Account Access| F[Filter Tier 1 Agents] C -->|Integration| G[Filter Integration Specialists] H[Customer Tier] --> I{Enterprise?} I -->|Yes| J[Filter Enterprise Team] I -->|No| K[Filter General Team] D --> L[Calculate Match Scores] E --> L F --> L G --> L J --> L K --> L L --> M[Select Best Match] M --> N[Check Availability] N --> O{Available?} O -->|Yes| P[Assign Ticket] O -->|No| Q[Select Next Best] Q --> N style B fill:#e1f5ff style L fill:#fff4e1 style P fill:#e8f5e9

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.

erDiagram TICKETS ||--o{ TICKET_INTENT_CLASSIFICATION : classifies TICKETS ||--o{ TICKET_AGENT_MATCHES : matches TICKET_INTENT_CLASSIFICATION ||--o{ TICKET_AGENT_MATCHES : influences AGENT_EXPERTISE ||--o{ TICKET_AGENT_MATCHES : matches_to TICKET_AGENT_MATCHES ||--o{ TICKET_ASSIGNMENTS : assigns TICKET_ASSIGNMENTS ||--o{ ROUTING_PERFORMANCE : tracks ROUTING_FEEDBACK ||--o{ ROUTING_PERFORMANCE : improves TICKETS { string ticket_id PK string subject string description string customer_email timestamp created_at } TICKET_INTENT_CLASSIFICATION { string ticket_id PK string intent_category float intent_confidence } AGENT_EXPERTISE { string agent_id PK string team jsonb expertise_areas integer current_workload boolean is_available } TICKET_ASSIGNMENTS { string ticket_id PK string assigned_agent_id string assigned_team float match_score }

Key relationships:

  • ticketsticket_intent_classification (classifies intent)
  • ticket_intent_classification + agent_expertiseticket_agent_matches (calculates matches)
  • ticket_agent_matchesticket_assignments (selects best match)
  • ticket_assignmentsrouting_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:

  1. Intent classification is foundational: Accurately classify what tickets are about. This determines which agents can handle them.

  2. Expertise mapping enables matching: Track agent skills and specializations. Match tickets to agents who excel at those topics.

  3. Context matters: Consider customer tier, priority, and special requirements. Enterprise customers need different routing than self-serve.

  4. Workload balancing prevents overload: Distribute tickets evenly. Don't overload some agents while others are idle.

  5. 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.