🚀

We've launched on Product Hunt

Check us out →
Tutorials

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

H

Hoshang Mehta

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:

  • Who is the customer? (Check CRM)
  • What's their plan tier? (Check billing)
  • What's their usage history? (Check product analytics)
  • Have they had similar issues before? (Check ticket history)
  • Are they a high-value customer? (Check revenue data)

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.

graph TD A[New Ticket Arrives] --> B[Ticket Ingestion Layer] B --> C[Enrichment Stage] C --> D[Customer Context Lookup] C --> E[Ticket History Lookup] C --> F[System Status Check] D --> G[Priority Scoring] E --> G F --> G G --> H[Routing Decision] H --> I[Assignment to Team/Agent] I --> J[Triage Dashboard] J --> K[Support Agent] style C fill:#e1f5ff style G fill:#fff4e1 style H fill:#e8f5e9

Architecture Components

1. Ticket Ingestion Layer

  • Connects to support systems (Zendesk, Intercom, custom ticketing)
  • Captures new tickets in real-time
  • Normalizes ticket format across systems
  • Stores raw ticket data for audit trail

2. Enrichment Stage

  • Queries customer context from CRM (HubSpot, Salesforce)
  • Retrieves billing status from payment systems (Stripe, billing DB)
  • Fetches product usage data from analytics (Postgres, Snowflake)
  • Looks up ticket history from support system
  • Checks system status for known issues

3. Priority Scoring Engine

  • Analyzes ticket content (subject, description)
  • Considers customer value (MRR, plan tier)
  • Factors in ticket history (repeat issues, satisfaction scores)
  • Checks system status (known outages, incidents)
  • Generates priority score (0-100)

4. Routing Logic

  • Classifies ticket intent (billing, technical, feature request)
  • Matches intent to team expertise
  • Considers agent workload
  • Applies business rules (enterprise customers → dedicated team)
  • Assigns ticket to optimal team/agent

5. Triage Dashboard

  • Real-time view of triaged tickets
  • Priority queue visualization
  • Assignment tracking
  • Performance metrics

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 a unified view that joins customer data across systems:

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:

  • Customer tier (enterprise, mid-market, self-serve)
  • Billing status and payment history
  • Product usage patterns
  • Support history (previous tickets, resolution times)
  • Risk indicators (billing issues, inactivity, repeat issues)

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)

  • Keywords: "critical", "down", "broken", "urgent" → +20 points
  • Sentiment: Negative sentiment → +10 points
  • Length: Very short or very long → +5 points (indicates frustration or complexity)

2. Customer Value (0-30 points)

  • Enterprise customer (MRR > $10K) → +20 points
  • Mid-market (MRR $1K-$10K) → +10 points
  • High support load (5+ open tickets) → +10 points

3. Historical Patterns (0-20 points)

  • Repeat issue (same problem 3+ times) → +15 points
  • Low satisfaction score (< 3/5) → +10 points
  • Recent escalation → +10 points

4. System Status (0-10 points)

  • Known outage affecting customer → +10 points
  • System-wide issue → +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

graph LR A[Ticket Content] --> B[Content Analysis] C[Customer Context] --> D[Value Scoring] E[Ticket History] --> F[Pattern Detection] G[System Status] --> H[Status Check] B --> I[Priority Calculator] D --> I F --> I H --> I I --> J[Priority Score 0-100] J --> K{Priority Category} K -->|60+| L[Critical] K -->|40-59| M[High] K -->|20-39| N[Medium] K -->|<20| O[Low] style I fill:#fff4e1 style J fill:#e8f5e9

Priority thresholds:

  • Critical (60+): Enterprise customer + critical keywords + system issue
  • High (40-59): Mid-market customer + urgent keywords
  • Medium (20-39): Standard customer + issue keywords
  • Low (<20): General inquiries, feature requests

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

graph TD A[Ticket with Priority Score] --> B{Intent Classification} B -->|Billing| C[Billing Team] B -->|Technical| D{Priority Level} B -->|Account Access| E[Tier 1 Support] B -->|How-to| E B -->|Feature Request| F[Product Team] B -->|General| E D -->|Critical/High| G[Engineering Support] D -->|Medium/Low| H[Tier 2 Support] I{Customer Tier} -->|Enterprise| J[Enterprise Support] C --> K[Agent Assignment] G --> K H --> K E --> K F --> K J --> K K --> L[Update Ticket Status] style B fill:#e1f5ff style I fill:#fff4e1 style K fill:#e8f5e9

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.

erDiagram TICKETS ||--o{ TICKET_PRIORITY_SCORES : has TICKETS ||--o{ TICKET_INTENT_CLASSIFICATION : has TICKETS ||--o{ TICKET_ASSIGNMENTS : has CUSTOMERS ||--o{ CUSTOMER_CONTEXT_COMPREHENSIVE : enriches CUSTOMER_CONTEXT_COMPREHENSIVE ||--o{ TICKET_PRIORITY_SCORES : influences TICKET_PRIORITY_SCORES ||--o{ TICKET_ASSIGNMENTS : determines TICKET_INTENT_CLASSIFICATION ||--o{ TICKET_ASSIGNMENTS : influences AGENTS ||--o{ TICKET_ASSIGNMENTS : assigned_to TICKETS { string ticket_id PK string subject string description string customer_email timestamp created_at string status } CUSTOMER_CONTEXT_COMPREHENSIVE { string customer_id PK string customer_name string email string customer_tier decimal mrr int open_tickets string support_load_category } TICKET_PRIORITY_SCORES { string ticket_id PK int content_score int customer_value_score int historical_score int system_status_score int priority_score string priority_category } TICKET_ASSIGNMENTS { string ticket_id PK string intent_category string assigned_team string assigned_agent_id } TRIAGE_DASHBOARD { string ticket_id PK int priority_score string priority_category string customer_tier string assigned_team string sla_status }

Key relationships:

  • tickets → raw ticket data from support systems
  • customer_context_comprehensive → unified customer data from CRM, billing, product, support
  • ticket_priority_scores → calculated priority scores based on content, customer value, history
  • ticket_assignments → routing decisions and agent assignments
  • triage_dashboard → aggregated view for real-time monitoring

Traceability: Every triaged ticket can be traced back to its raw source, enrichment data, scoring logic, and assignment decision.


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.


This architecture pattern is implemented in Pylar, but the concepts apply to any system processing support tickets. The key is the pipeline approach: ingest → enrich → score → route → assign.