Building a Scalable Support Ticket Triage Pipeline: From Inbox to Resolution
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.
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
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
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.
Key relationships:
tickets→ raw ticket data from support systemscustomer_context_comprehensive→ unified customer data from CRM, billing, product, supportticket_priority_scores→ calculated priority scores based on content, customer value, historyticket_assignments→ routing decisions and agent assignmentstriage_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:
-
Enrichment is foundational: Customer context makes everything else possible. Invest in building comprehensive customer views that join data across systems.
-
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.
-
Routing rules evolve: Start with simple rules, then refine based on what you learn. Track routing accuracy and adjust assignments.
-
Real-time matters: Tickets need triage within minutes, not hours. Materialized views with frequent refreshes enable near-real-time processing.
-
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.