Implementing Intelligent Support Ticket Routing

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:

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:

The impact: Agent A's response time increases, customers wait longer, agent burnout increases.

The Expertise Mismatch Problem

Generalists handle specialized issues:

The cost: Specialized agents are underutilized. Complex issues take longer to resolve.

The Context Loss Problem

Routing decisions don't consider customer context:

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

2. Customer Context Integration

3. Expertise Mapping System

4. Matching Algorithm

5. Performance Tracking


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

Billing

Technical

Account Access

Integration

Yes

No

Yes

No

New Ticket

Intent Classification

Intent Category

Filter Billing Agents

Filter Technical Agents

Filter Tier 1 Agents

Filter Integration Specialists

Customer Tier

Enterprise?

Filter Enterprise Team

Filter General Team

Calculate Match Scores

Select Best Match

Check Availability

Available?

Assign Ticket

Select Next Best


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:

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.