Building a Scalable Support Ticket Triage Pipeline

Building a Scalable Support Ticket Triage Pipeline: From Inbox to Resolution

Support teams receive hundreds of tickets daily. Manual triage is slow and inconsistent—high-priority tickets get buried, agents lack customer context, and teams can't identify patterns. The result: frustrated customers, overwhelmed support teams, and missed SLAs.

The problem: A support agent opens their inbox to 50 new tickets. They manually read each one, check customer data in three different systems, guess at priority, and route to teams. By the time they finish, 20 more tickets have arrived. Critical issues from enterprise customers sit unread for hours.

The solution: An automated triage pipeline that analyzes incoming tickets, enriches them with customer context from CRM and billing systems, scores priority using ML models, and routes to the right team—all in seconds. This post walks through building a repeatable architecture that transforms ticket triage from manual chaos into automated intelligence.


The Problem with Manual Ticket Triage

Manual ticket triage breaks down at scale. Here's what happens:

The Context Gathering Problem

When a ticket arrives, support agents need to understand:

The math: If each context check takes 2 minutes and you receive 100 tickets per day, that's 200 minutes (3.3 hours) just gathering context—before solving any problems.

The Priority Guessing Problem

Priority assignment is subjective. One agent marks "Can't log in" as high priority. Another marks it as medium. Enterprise customers get the same treatment as free users. Critical issues from high-value customers get buried.

The impact: A $10K/month customer's critical issue sits in the queue for 4 hours while a free user's low-priority question gets answered first.

The Routing Problem

Tickets get routed to the wrong teams. A billing question goes to technical support. A bug report goes to sales. Agents waste time reassigning tickets instead of solving them.

The cost: 30% of tickets require reassignment, adding 15 minutes of delay per ticket. For 100 tickets, that's 25 hours of wasted time per day.

The Pattern Detection Problem

Teams can't identify patterns. If 20 customers report the same bug, each ticket is treated individually. No one notices the trend until it becomes a crisis.

The missed opportunity: Early detection of patterns could prevent 80% of escalations, but manual triage makes pattern detection impossible.


A Repeatable Architecture for Ticket Triage

The solution is a pipeline architecture that processes tickets through four stages: ingestion, enrichment, scoring, and routing.

  1. New Ticket Arrives
  2. Ticket Ingestion Layer
  3. Enrichment Stage
    • Customer Context Lookup
    • Ticket History Lookup
    • System Status Check
  4. Priority Scoring
  5. Routing Decision
  6. Assignment to Team/Agent
  7. Triage Dashboard
    • Support Agent

Architecture Components

1. Ticket Ingestion Layer

2. Enrichment Stage

3. Priority Scoring Engine

4. Routing Logic

5. Triage Dashboard


Enriching Tickets with Customer Context

The first stage of triage is enriching tickets with customer context. Instead of agents manually checking multiple systems, the pipeline automatically gathers all relevant data.

Building the Customer Context View

CREATE MATERIALIZED VIEW customer_context_comprehensive AS
SELECT
    -- From CRM (HubSpot/Salesforce)
    c.customer_id,
    c.customer_name,
    c.email,
    c.company_name,
    c.account_owner,
    c.plan_name,
    c.mrr,
    c.contract_value,
    c.renewal_date,
    c.customer_segment,

-- From Billing (Stripe/Billing DB)
    b.billing_status,
    b.last_payment_date,
    b.payment_method,
    b.days_since_last_payment,
    b.total_revenue,

-- From Product Analytics (Postgres/Snowflake)
    p.active_users,
    p.last_login_date,
    p.days_since_last_login,
    p.feature_adoption_score,
    p.usage_trend,

-- From Support History
    s.total_tickets,
    s.open_tickets,
    s.avg_resolution_time_hours,
    s.last_ticket_date,
    s.last_ticket_type,
    s.satisfaction_score_avg,
    s.repeat_issue_count,

-- Calculated Fields
    CASE
        WHEN c.mrr > 10000 THEN 'enterprise'
        WHEN c.mrr > 1000 THEN 'mid_market'
        ELSE 'self_serve'
    END as customer_tier,

CASE
        WHEN s.open_tickets > 5 THEN 'high_support_load'
        WHEN s.open_tickets > 2 THEN 'medium_support_load'
        ELSE 'low_support_load'
    END as support_load_category,

CASE
        WHEN b.billing_status != 'current' THEN 'billing_issue'
        WHEN p.days_since_last_login > 30 THEN 'inactive'
        WHEN s.repeat_issue_count > 3 THEN 'repeat_issues'
        ELSE 'healthy'
    END as risk_indicators,

CURRENT_TIMESTAMP as context_refreshed_at

FROM customers c
LEFT JOIN billing_status b ON c.customer_id = b.customer_id
LEFT JOIN product_usage_metrics p ON c.customer_id = p.customer_id
LEFT JOIN support_history_summary s ON c.customer_id = s.customer_id
WHERE c.customer_status = 'active';

Enrichment Process

When a ticket arrives, the pipeline queries this view:

-- Enrich ticket with customer context
SELECT
    t.ticket_id,
    t.subject,
    t.description,
    t.created_at,
    t.customer_email,
    cc.*  -- All customer context fields
FROM tickets t
LEFT JOIN customer_context_comprehensive cc
    ON t.customer_email = cc.email
WHERE t.ticket_id = :ticket_id;

What gets enriched:

Refresh strategy: Materialized view refreshes every 15 minutes for near-real-time context. For critical tickets, trigger on-demand refresh.


Priority Scoring with ML Models

Priority scoring determines which tickets need immediate attention. The score combines ticket content analysis, customer value, and historical patterns.

Priority Score Components

1. Content Analysis (0-40 points)

2. Customer Value (0-30 points)

3. Historical Patterns (0-20 points)

4. System Status (0-10 points)

Priority Score Calculation

CREATE MATERIALIZED VIEW ticket_priority_scores AS
SELECT
    t.ticket_id,
    t.subject,
    t.description,
    t.customer_email,
    t.created_at,

-- Content Analysis Score
    CASE
        WHEN LOWER(t.subject) LIKE '%critical%'
          OR LOWER(t.subject) LIKE '%down%'
          OR LOWER(t.subject) LIKE '%broken%'
          OR LOWER(t.subject) LIKE '%urgent%' THEN 20
        WHEN LOWER(t.subject) LIKE '%issue%'
          OR LOWER(t.subject) LIKE '%problem%' THEN 10
        ELSE 0
    END as content_score,

-- Customer Value Score
    CASE
        WHEN cc.customer_tier = 'enterprise' THEN 20
        WHEN cc.customer_tier = 'mid_market' THEN 10
        ELSE 0
    END +
    CASE
        WHEN cc.support_load_category = 'high_support_load' THEN 10
        WHEN cc.support_load_category = 'medium_support_load' THEN 5
        ELSE 0
    END as customer_value_score,

-- Historical Pattern Score
    CASE
        WHEN cc.repeat_issue_count > 3 THEN 15
        WHEN cc.repeat_issue_count > 1 THEN 10
        ELSE 0
    END +
    CASE
        WHEN cc.satisfaction_score_avg < 3 THEN 10
        ELSE 0
    END as historical_score,

-- System Status Score
    CASE
        WHEN ss.status != 'operational' AND ss.affected_customers LIKE '%' || cc.customer_id || '%' THEN 10
        ELSE 0
    END as system_status_score,

-- Total Priority Score
    (
        CASE WHEN LOWER(t.subject) LIKE '%critical%' OR LOWER(t.subject) LIKE '%down%' OR LOWER(t.subject) LIKE '%broken%' OR LOWER(t.subject) LIKE '%urgent%' THEN 20
        WHEN LOWER(t.subject) LIKE '%issue%' OR LOWER(t.subject) LIKE '%problem%' THEN 10 ELSE 0 END +
        CASE WHEN cc.customer_tier = 'enterprise' THEN 20
        WHEN cc.customer_tier = 'mid_market' THEN 10 ELSE 0 END +
        CASE WHEN cc.support_load_category = 'high_support_load' THEN 10
        WHEN cc.support_load_category = 'medium_support_load' THEN 5 ELSE 0 END +
        CASE WHEN cc.repeat_issue_count > 3 THEN 15
        WHEN cc.repeat_issue_count > 1 THEN 10 ELSE 0 END +
        CASE WHEN cc.satisfaction_score_avg < 3 THEN 10 ELSE 0 END +
        CASE WHEN ss.status != 'operational' AND ss.affected_customers LIKE '%' || cc.customer_id || '%' THEN 10 ELSE 0 END
    ) as priority_score,

-- Priority Category
    CASE
        WHEN (
            CASE WHEN LOWER(t.subject) LIKE '%critical%' OR LOWER(t.subject) LIKE '%down%' OR LOWER(t.subject) LIKE '%broken%' OR LOWER(t.subject) LIKE '%urgent%' THEN 20
            WHEN LOWER(t.subject) LIKE '%issue%' OR LOWER(t.subject) LIKE '%problem%' THEN 10 ELSE 0 END +
            CASE WHEN cc.customer_tier = 'enterprise' THEN 20
            WHEN cc.customer_tier = 'mid_market' THEN 10 ELSE 0 END +
            CASE WHEN cc.support_load_category = 'high_support_load' THEN 10
            WHEN cc.support_load_category = 'medium_support_load' THEN 5 ELSE 0 END +
            CASE WHEN cc.repeat_issue_count > 3 THEN 15
            WHEN cc.repeat_issue_count > 1 THEN 10 ELSE 0 END +
            CASE WHEN cc.satisfaction_score_avg < 3 THEN 10 ELSE 0 END +
            CASE WHEN ss.status != 'operational' AND ss.affected_customers LIKE '%' || cc.customer_id || '%' THEN 10 ELSE 0 END
        ) >= 60 THEN 'critical'
        WHEN (
            CASE WHEN LOWER(t.subject) LIKE '%critical%' OR LOWER(t.subject) LIKE '%down%' OR LOWER(t.subject) LIKE '%broken%' OR LOWER(t.subject) LIKE '%urgent%' THEN 20
            WHEN LOWER(t.subject) LIKE '%issue%' OR LOWER(t.subject) LIKE '%problem%' THEN 10 ELSE 0 END +
            CASE WHEN cc.customer_tier = 'enterprise' THEN 20
            WHEN cc.customer_tier = 'mid_market' THEN 10 ELSE 0 END +
            CASE WHEN cc.support_load_category = 'high_support_load' THEN 10
            WHEN cc.support_load_category = 'medium_support_load' THEN 5 ELSE 0 END +
            CASE WHEN cc.repeat_issue_count > 3 THEN 15
            WHEN cc.repeat_issue_count > 1 THEN 10 ELSE 0 END +
            CASE WHEN cc.satisfaction_score_avg < 3 THEN 10 ELSE 0 END +
            CASE WHEN ss.status != 'operational' AND ss.affected_customers LIKE '%' || cc.customer_id || '%' THEN 10 ELSE 0 END
        ) >= 40 THEN 'high'
        WHEN (
            CASE WHEN LOWER(t.subject) LIKE '%critical%' OR LOWER(t.subject) LIKE '%down%' OR LOWER(t.subject) LIKE '%broken%' OR LOWER(t.subject) LIKE '%urgent%' THEN 20
            WHEN LOWER(t.subject) LIKE '%issue%' OR LOWER(t.subject) LIKE '%problem%' THEN 10 ELSE 0 END +
            CASE WHEN cc.customer_tier = 'enterprise' THEN 20
            WHEN cc.customer_tier = 'mid_market' THEN 10 ELSE 0 END +
            CASE WHEN cc.support_load_category = 'high_support_load' THEN 10
            WHEN cc.support_load_category = 'medium_support_load' THEN 5 ELSE 0 END +
            CASE WHEN cc.repeat_issue_count > 3 THEN 15
            WHEN cc.repeat_issue_count > 1 THEN 10 ELSE 0 END +
            CASE WHEN cc.satisfaction_score_avg < 3 THEN 10 ELSE 0 END +
            CASE WHEN ss.status != 'operational' AND ss.affected_customers LIKE '%' || cc.customer_id || '%' THEN 10 ELSE 0 END
        ) >= 20 THEN 'medium'
        ELSE 'low'
    END as priority_category

FROM tickets t
LEFT JOIN customer_context_comprehensive cc ON t.customer_email = cc.email
LEFT JOIN system_status ss ON ss.affected_system = t.category
WHERE t.status = 'new';

Priority Scoring Flow


Routing Logic and Assignment

Once priority is scored, tickets need to be routed to the right team. Routing considers ticket intent, team expertise, and agent workload.

Intent Classification

First, classify what the ticket is about:

CREATE MATERIALIZED VIEW ticket_intent_classification AS
SELECT
    t.ticket_id,
    t.subject,
    t.description,

-- Intent Classification
    CASE
        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%' THEN 'billing'
        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%' THEN 'technical'
        WHEN LOWER(t.subject || ' ' || t.description) LIKE '%feature%'
          OR LOWER(t.subject || ' ' || t.description) LIKE '%request%'
          OR LOWER(t.subject || ' ' || t.description) LIKE '%suggestion%' THEN 'feature_request'
        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%' THEN 'how_to'
        WHEN LOWER(t.subject || ' ' || t.description) LIKE '%account%'
          OR LOWER(t.subject || ' ' || t.description) LIKE '%login%'
          OR LOWER(t.subject || ' ' || t.description) LIKE '%access%' THEN 'account_access'
        ELSE 'general'
    END as intent_category

FROM tickets t
WHERE t.status = 'new';

Team Assignment Rules

CREATE MATERIALIZED VIEW ticket_assignments AS
SELECT
    tps.ticket_id,
    tps.priority_score,
    tps.priority_category,
    tic.intent_category,
    cc.customer_tier,

-- Assignment Logic
    CASE
        -- Enterprise customers → dedicated team
        WHEN cc.customer_tier = 'enterprise' THEN 'enterprise_support'

-- Billing issues → billing team
        WHEN tic.intent_category = 'billing' THEN 'billing_support'

-- Technical bugs → engineering support
        WHEN tic.intent_category = 'technical' AND tps.priority_category IN ('critical', 'high') THEN 'engineering_support'

-- Technical issues → tier 2 support
        WHEN tic.intent_category = 'technical' THEN 'tier_2_support'

-- Account access → tier 1 support
        WHEN tic.intent_category = 'account_access' THEN 'tier_1_support'

-- How-to questions → tier 1 support
        WHEN tic.intent_category = 'how_to' THEN 'tier_1_support'

-- Feature requests → product team
        WHEN tic.intent_category = 'feature_request' THEN 'product_team'

-- Default → tier 1 support
        ELSE 'tier_1_support'
    END as assigned_team,

-- Agent Assignment (within team, based on workload)
    (
        SELECT agent_id
        FROM agents a
        WHERE a.team = (
            CASE
                WHEN cc.customer_tier = 'enterprise' THEN 'enterprise_support'
                WHEN tic.intent_category = 'billing' THEN 'billing_support'
                WHEN tic.intent_category = 'technical' AND tps.priority_category IN ('critical', 'high') THEN 'engineering_support'
                WHEN tic.intent_category = 'technical' THEN 'tier_2_support'
                WHEN tic.intent_category = 'account_access' THEN 'tier_1_support'
                WHEN tic.intent_category = 'how_to' THEN 'tier_1_support'
                WHEN tic.intent_category = 'feature_request' THEN 'product_team'
                ELSE 'tier_1_support'
            END
        )
        AND a.status = 'available'
        ORDER BY a.open_tickets ASC, a.last_assigned_at ASC
        LIMIT 1
    ) as assigned_agent_id

FROM ticket_priority_scores tps
LEFT JOIN ticket_intent_classification tic ON tps.ticket_id = tic.ticket_id
LEFT JOIN tickets t ON tps.ticket_id = t.ticket_id
LEFT JOIN customer_context_comprehensive cc ON t.customer_email = cc.email;

Routing Decision Flow


Real-Time Triage Dashboard

The triage dashboard gives support teams visibility into the pipeline. It shows triaged tickets, priority queues, and performance metrics.

Dashboard View Structure

CREATE MATERIALIZED VIEW triage_dashboard AS
SELECT
    -- Ticket Info
    t.ticket_id,
    t.subject,
    t.customer_email,
    t.created_at,
    t.status,

-- Priority Info
    tps.priority_score,
    tps.priority_category,

-- Customer Context
    cc.customer_name,
    cc.customer_tier,
    cc.mrr,
    cc.support_load_category,

-- Intent & Assignment
    tic.intent_category,
    ta.assigned_team,
    ta.assigned_agent_id,

-- Timing
    EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - t.created_at)) / 60 as minutes_since_created,
    CASE
        WHEN tps.priority_category = 'critical' AND EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - t.created_at)) / 60 > 30 THEN 'overdue'
        WHEN tps.priority_category = 'high' AND EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - t.created_at)) / 60 > 60 THEN 'overdue'
        WHEN tps.priority_category = 'medium' AND EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - t.created_at)) / 60 > 240 THEN 'overdue'
        ELSE 'on_time'
    END as sla_status

FROM tickets t
LEFT JOIN ticket_priority_scores tps ON t.ticket_id = tps.ticket_id
LEFT JOIN ticket_intent_classification tic ON t.ticket_id = tic.ticket_id
LEFT JOIN ticket_assignments ta ON t.ticket_id = ta.ticket_id
LEFT JOIN customer_context_comprehensive cc ON t.customer_email = cc.email
WHERE t.status IN ('new', 'assigned', 'in_progress');

MCP Tool for Triage Dashboard

{
  "name": "get_triage_queue",
  "description": "Retrieves the current triage queue with priority scores, customer context, and assignments. Use when support teams need to see what tickets need attention, filter by priority, team, or customer tier.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "priority_category": {
        "type": "string",
        "enum": ["critical", "high", "medium", "low", "all"],
        "description": "Filter by priority category. Defaults to 'all'.",
        "default": "all"
      },
      "assigned_team": {
        "type": "string",
        "description": "Filter by assigned team (e.g., 'tier_1_support', 'billing_support')"
      },
      "customer_tier": {
        "type": "string",
        "enum": ["enterprise", "mid_market", "self_serve", "all"],
        "description": "Filter by customer tier. Defaults to 'all'.",
        "default": "all"
      },
      "limit": {
        "type": "integer",
        "description": "Maximum number of results to return",
        "minimum": 1,
        "maximum": 100,
        "default": 50
      }
    }
  },
  "query": "SELECT * FROM triage_dashboard WHERE (priority_category = $1 OR $1 = 'all') AND (assigned_team = $2 OR $2 IS NULL) AND (customer_tier = $3 OR $3 = 'all') ORDER BY priority_score DESC, created_at ASC LIMIT $4",
  "policies": ["authenticated", "role:support OR role:manager"]
}

Schema Overview

The triage pipeline uses a hierarchical schema that traces from raw tickets to enriched, scored, and assigned tickets.


Closing Thoughts

Building a scalable ticket triage pipeline transforms support operations from reactive to proactive. The key is treating triage as a data pipeline problem—not a manual process.

Key takeaways:

  1. Enrichment is foundational: Customer context makes everything else possible. Invest in building comprehensive customer views that join data across systems.
  2. Scoring should be transparent: Priority scores should be explainable. Support teams need to understand why a ticket got a certain score, not just the score itself.
  3. Routing rules evolve: Start with simple rules, then refine based on what you learn. Track routing accuracy and adjust assignments.
  4. Real-time matters: Tickets need triage within minutes, not hours. Materialized views with frequent refreshes enable near-real-time processing.
  5. Monitor and iterate: Track triage performance—priority accuracy, routing accuracy, SLA compliance. Use this data to improve the pipeline.

The architecture described here is repeatable. Start with enrichment and priority scoring, then add routing and assignment. Each stage builds on the previous one, and you can deploy incrementally.

Most importantly: this isn't about replacing support agents—it's about giving them superpowers. Agents spend less time gathering context and more time solving problems. High-priority tickets get attention faster. Patterns get detected earlier. Support becomes proactive instead of reactive.