GTM Operations
58% of Your Support Tickets Should Not Exist
I pulled 90 days of a support queue and bucketed every ticket by root cause. Most of the volume was seven questions asked over and over. Here is the query and the deflection loop that drains it.
· 14 min read
1,900 tickets in 90 days. That was the number the support lead brought to the staffing meeting, and it came with a request for two more agents. I asked for read access to the queue instead of signing off on the headcount. When I bucketed every one of those tickets by what caused it, 1,160 of them traced back to seven questions. Same seven. Asked over and over by different accounts, answered from scratch each time by an agent typing the same paragraph they typed yesterday.
The queue was not understaffed. It was uninstrumented. Nobody had ever asked the queue what it was made of, so it looked like demand when most of it was a documentation gap and two product bugs wearing a thousand costumes. This is the autopsy: how I bucket a support queue by root cause, the query that does it, and the loop that drains the deflectable half instead of hiring against it.
Here is the whole autopsy in one image. Watch 1,900 tickets, a pile that looks like demand, dissolve down to the seven root causes behind it, and then to the handful of upstream fixes that drain it. That collapse is the argument, and the deflection loop later in this piece is the machine that produces it.
Volume is not demand
The mistake baked into every staffing request is treating ticket count as a measure of how much support customers need. Ticket count measures something else: how many times your product, your docs, and your onboarding failed to answer a question before it reached a human. A queue of 1,900 can be 1,900 distinct problems that each deserve an agent, or it can be seven problems multiplied by bad self-service. You cannot tell which from the count. You can only tell by bucketing.
So the first move is never “how many tickets,” it is “how many kinds of ticket, and which kinds repeat.” A repeating ticket is a signal that something upstream is broken and support is absorbing the cost of not fixing it. Every time an agent answers the same password-reset-for-SSO question, the company pays twice: once for the agent’s time and once for the customer’s friction. That friction is not free at renewal. Expansion costs $0.80 per dollar of ARR against $1.63 to acquire (Aleph and Benchmarkit, 2026), so the accounts you annoy with a preventable wall are the cheapest revenue you have, spent down one ticket at a time. Multiply by 1,160 and the staffing meeting is solving the wrong problem out loud.
One caveat on benchmarks here. The clean 2026 CX reports still do not publish reliable cross-industry medians for first-response time, handle time, or deflection rate, so I will not pretend a number is a standard when it is not. What travels is the method, not a magic deflection percentage: bucket your own queue, and the shape below is what falls out almost every time.
Bucket the queue by root cause, not by product area
Most support taxonomies bucket by product area (Billing, Login, API, Reports) because that is how teams are organized. That grouping tells you which team is busy. It does not tell you what to fix. The bucketing that matters is by root cause: is this ticket here because the answer was undiscoverable, because the product did something surprising, because a workflow is genuinely hard, or because the customer hit a real bug?
Four root-cause classes cover almost everything I have pulled:
- Doc gap. The answer exists or could exist as a doc, and the customer could have self-served. Fully deflectable.
- Product friction. The product works as designed but the design surprises people. Deflectable by changing the product, not by writing more docs.
- Genuine bug. Something is broken. Not deflectable by docs; the ticket is doing its job by surfacing the defect. Fix the bug, the tickets stop.
- True assist. A real question that needs a human, specific to the account. This is the demand you staff for.
The staffing request assumed the whole queue was that last class. Here is what the bucketing found instead.
The query that does the bucketing
You do not need a data science project for this. If support runs on Salesforce Service Cloud, the Case object already carries the fields you need, and a GROUP BY gives you the first cut in one query. The trick is to bucket on a normalized cause field, not on free-text subjects. If nobody has been setting a cause field, start with Type and Reason and refine from there.
SELECT Type, Reason, COUNT(Id) tickets, AVG(Time_to_Resolution__c) avg_hrs
FROM Case
WHERE CreatedDate = LAST_N_DAYS:90
GROUP BY Type, Reason
ORDER BY COUNT(Id) DESC
That surfaces the shape of the queue in seconds. But Type and Reason are coarse, and the real repetition hides in the subject lines. To find the seven-questions pattern, you have to cluster the free text. I pull the raw cases and normalize the subject into a cause bucket in the warehouse, where I have string functions SOQL does not give me:
-- warehouse: 90 days of cases, bucketed to a root cause
SELECT
CASE
WHEN LOWER(subject) LIKE '%sso%' OR LOWER(subject) LIKE '%saml%'
OR LOWER(subject) LIKE '%reset password%' THEN 'SSO / password reset'
WHEN LOWER(subject) LIKE '%invoice%' OR LOWER(subject) LIKE '%receipt%'
OR LOWER(subject) LIKE '%billing address%' THEN 'Self-serve billing docs'
WHEN LOWER(subject) LIKE '%export%' OR LOWER(subject) LIKE '%csv%'
OR LOWER(subject) LIKE '%download report%' THEN 'Report export'
WHEN LOWER(subject) LIKE '%api key%' OR LOWER(subject) LIKE '%401%'
OR LOWER(subject) LIKE '%token expired%' THEN 'API auth (bug)'
WHEN LOWER(subject) LIKE '%seat%' OR LOWER(subject) LIKE '%add user%'
OR LOWER(subject) LIKE '%permission%' THEN 'Seat / role management'
WHEN LOWER(subject) LIKE '%timezone%' OR LOWER(subject) LIKE '%wrong date%' THEN 'Timezone display (bug)'
WHEN LOWER(subject) LIKE '%cancel%' OR LOWER(subject) LIKE '%downgrade%' THEN 'Plan change'
ELSE 'Unclassified / true assist'
END AS root_cause,
COUNT(*) AS tickets,
ROUND(AVG(time_to_resolution_hrs), 1) AS avg_hrs,
COUNT(DISTINCT account_id) AS accounts
FROM support.cases
WHERE created_date >= DATEADD('day', -90, CURRENT_DATE)
GROUP BY 1
ORDER BY tickets DESC;
The LIKE clauses are ugly and they are supposed to be. You tune them by reading the top 50 subjects, adding patterns until the “Unclassified” bucket shrinks to the tickets that genuinely need a human. When Unclassified stops dropping as you add rules, you have found your true-assist floor. Everything above it is a candidate for deflection.
What the buckets said
Here is the 90-day queue after bucketing. The account count matters as much as the ticket count: a bucket where 40 accounts filed 400 tickets is a repeat-offender pattern, and a bucket where 380 accounts filed 400 tickets is broad friction. Both are deflectable, but you fix them differently.
| Root cause | Tickets | Share | Accounts | Class | Deflectable |
|---|---|---|---|---|---|
| SSO / password reset | 310 | 16% | 44 | Doc gap | Yes |
| Self-serve billing docs | 240 | 13% | 210 | Doc gap | Yes |
| Report export | 180 | 9% | 160 | Product friction | Yes |
| API auth (401 loop) | 170 | 9% | 22 | Genuine bug | Fix bug |
| Seat / role management | 110 | 6% | 95 | Doc gap | Yes |
| Timezone display | 90 | 5% | 71 | Genuine bug | Fix bug |
| Plan change | 60 | 3% | 58 | Product friction | Yes |
| Unclassified / true assist | 740 | 39% | 610 | True assist | No |
Source: 90-day support.cases pull, bucketing query above.
Add the deflectable rows and the two bug rows and you get 1,160 tickets, 61% of the queue, that exist because of something fixable upstream. I called it 58% in the headline because 40 of the SSO tickets turned out to be genuine account lockouts that needed a human, so I moved them to true assist after reading them. The honest number lands between the two. Either way, the majority of the queue is the cost of seven unfixed things, not demand.
The pattern is Pareto and it always is
Notice the shape. Two doc-gap buckets and one product-friction bucket (SSO, billing, export) account for 730 tickets on their own. Seven root causes account for 1,160. The long tail of true assist is real work, but the front of the distribution is a handful of things repeating. Support queues are Pareto almost without exception, and the payoff is entirely at the front. Fix the top three and the queue does not shrink 5%, it shrinks a third.
View as table
| Item | Value |
|---|---|
| SSO / pw | 310 |
| Billing docs | 240 |
| Report export | 180 |
| API auth bug | 170 |
| Seat mgmt | 110 |
| Timezone bug | 90 |
Deflect the cause, never the symptom
The fastest way to ruin a deflection program is to deflect the symptom. You see 310 SSO tickets, you build a chatbot that answers SSO questions, and you feel productive. But the customer still hit the wall, still got frustrated, still churned a little in their head before the bot caught them. You moved the cost from an agent to a bot; you did not remove it. Real deflection removes the reason the question got asked.
For each root cause there is a different upstream fix, and matching the fix to the class is the entire skill:
| Root cause | Wrong fix (symptom) | Right fix (cause) | Owner |
|---|---|---|---|
| SSO / password reset | Macro reply, chatbot answer | Inline SSO error with a fix link at the point of failure | Product + Docs |
| Self-serve billing docs | Agent pastes the same steps | Self-serve invoice download in the billing UI | Product |
| Report export | Video walkthrough in a reply | Export button where users look for it, not buried in a menu | Product |
| API auth (401 loop) | “Try regenerating your key” macro | Fix the token-refresh bug | Engineering |
| Seat / role management | FAQ article | Guided seat-invite flow with role tooltips | Product |
Source: root-cause mapping from the bucketed queue.
Three of the top five fixes are product changes, not documentation. That is the uncomfortable finding in most support autopsies: the queue is largely a product-quality report that nobody reads as one. Support is where product debt surfaces as human labor. The CS Ops function that owns the post-sale system has to route these findings back to product with the ticket count attached, or the queue refills.
| Before the autopsy | After the loop | |
|---|---|---|
| How the queue is reported | One number, trending up | Bucketed by root cause, weekly |
| Top-3 buckets | 730 tickets, unexamined | Retired via 2 product fixes + 3 docs |
| The two bugs | 260 tickets, treated as demand | Filed as P1s, tickets stopped at source |
| Staffing decision | Hire 2 agents | Redirect 1 eng-quarter + 1 writer |
| Queue 90 days later | Projected 2,100 | Actual 1,180 |
The loop that drains the queue
Deflection is not a one-time cleanup. Docs rot, products ship new surprises, and new root causes climb the Pareto curve as you retire the old ones. So you run a loop, not a project. I run it on the organize, adopt, report, optimize cadence, which maps cleanly onto how a support queue breathes. Each turn of the loop reads the queue, fixes the top cause, and measures whether the fix held.
Here is how I would build it, in order, so the first fix ships inside two weeks and the loop is self-sustaining by month three.
- 1
1. Instrument the cause field
Add a picklist root-cause field to the Case object and make agents set it at close, or run the bucketing query nightly against subjects if you cannot change agent behavior yet. You need cause data before you can deflect anything. A queue you cannot bucket is a queue you cannot drain.
- 2
2. Pull the 90-day bucket report
Run the warehouse query, read the top 50 subjects, tune the LIKE rules until Unclassified is only true assist. Rank buckets by tickets and by distinct accounts. This is the autopsy that replaces the staffing argument.
- 3
3. Classify each top bucket by cause type
Doc gap, product friction, or bug. This decides who owns the fix. Do not skip this: a doc written for a product-friction bucket deflects nothing, because the customer never reaches the doc.
- 4
4. Ship the top-cause fix, not the top-count macro
Take the single largest deflectable bucket and fix the cause. Inline error link for SSO, self-serve download for billing, bug ticket for the 401 loop. One real fix beats ten macros.
- 5
5. Report deflection, not deflection theater
Re-pull the bucket after the fix ships. The metric is the drop in that bucket, measured in tickets that stopped arriving, not chatbot deflection-rate vanity. If the bucket did not shrink, you fixed the symptom.
- 6
6. Turn the loop monthly
Pick the new top cause and repeat. As you retire the front of the Pareto curve, the tail becomes the front. The queue trends down while headcount stays flat, which is the only support economics that scale.
Where this sits in the operating cadence
The autopsy is a monthly job, not a weekly one. A weekly review might clean ten stale tickets; a monthly review should explain why those stale tickets keep appearing (Clari, 2026 operating-cadence framework). In the operating cadence, the weekly support review clears the queue and hits SLA. The monthly review is where you ask the harder question the weekly cannot: why do these same tickets keep appearing? Weekly answers “are we keeping up.” Monthly answers “why is there so much to keep up with.” If you only run the weekly, you staff forever against a queue you could have drained. The bucketing report is the monthly artifact that turns clearing tickets into removing them.
The support lead got the read access, not the two agents. The next quarter’s pull showed 1,180 tickets against a projected 2,100, and the difference was three docs, two product fixes, and two bugs filed as P1s. Nobody was hired. The queue needed someone to ask it what it was made of, not more people. Pull your last 90 days with the query above, bucket it by cause, and I would bet the same shape falls out: a handful of unfixed things wearing a thousand costumes, and a staffing request pointed at the wrong problem.
Keep reading
One email. Every week.
One email a week: an operating problem I solved or botched, with the model, the numbers, and what I would change. No roundups, no theory, unsubscribe whenever it stops being useful.
The newsletter opens soon.
Connect a provider in src/config.ts