One customer complained eleven times in nine days.
Not eleven different problems. One problem — a delayed shipment. She emailed Monday, chatted Tuesday, emailed again Thursday, replied to her own email three times, submitted a web form, and contacted you on two different addresses because her first attempt got no reply.
Your AI handled every single message the same way: it read it, it "understood" it, and it opened a brand-new ticket.
Eleven tickets. Eleven ticket numbers. Eleven automated "we're on it!" confirmations. Zero of them linked together. When your human agent finally got involved on day nine, she read three tickets about three different-sounding issues, gave a partial answer on one, and closed it — while the other ten sat open and the customer kept getting escalation emails from tickets nobody was reading.
The compounding problem: most helpdesks auto-close tickets after N days of silence. So six of her eleven tickets closed as "resolved — no response." Your dashboard said 6 tickets solved that week. The customer said her problem was never solved. Both of you were looking at the same system.
Here's the part nobody budgets for: duplicate tickets aren't a storage problem, they're a routing and trust problem. Eleven open tickets means eleven entries in the agent queue, eleven chances for two agents to answer the same thing differently, and eleven "we'll get back to you" emails to one increasingly furious person.
I've audited enough of these deployments now that I can tell you the pattern before I open the dashboard. If your AI agent doesn't have an explicit dedup layer, you have duplicates. Not "you might." You do.
Here's how to fix it in an afternoon.
Step 1: Define what "duplicate" actually means for you
Vague dedup rules are worse than none, because they merge things that shouldn't be merged. A customer asking about the shipping delay and asking for an invoice copy in the same email is not a duplicate — it's two tickets, and merging them means one of them gets dropped.
Use this decision rule, and encode it as a prompt your agent evaluates before creating anything new:
A message is a duplicate when all four are true:
- Same identity — same customer (email, phone, or logged-in account; not just "similar name").
- Same subject — the underlying issue is the same entity: same order, same booking, same shipment, same invoice. "Same general topic" doesn't count.
- Same stage — the previous ticket isn't already resolved-and-confirmed. A new message after a confirmed resolution is not a duplicate; it's a follow-up or a regression, and it deserves a fresh ticket linked to the old one.
- No new money — the message doesn't contain a new refund request, a changed address, or a new escalation threat. New actionable content = merge the threads, but surface the new content on the canonical ticket.
If any one of the four fails, don't merge. Create a new ticket and link them instead.
That last part matters more than people expect. "Merge" and "link" are different operations. Merge = one canonical ticket absorbs the content. Link = tickets stay separate but reference each other. Merging the "shipping is late" ticket with the "change my delivery address" ticket is how address changes silently die inside logistics threads.
Step 2: The dedup prompt (drop this into your agent)
Give your agent a merge-check step that runs before ticket creation:
{
"dedup_check": {
"run_before": "create_ticket",
"search_scope": "open_tickets + tickets_closed_within_7_days",
"identity_match": ["customer_email", "customer_phone", "account_id"],
"duplicate_requires": {
"identity": "exact_match_required",
"subject": "same_entity_same_issue",
"stage": "previous_ticket_not_resolved_confirmed",
"new_money_or_contact_change": "if_true -> merge_threads_but_flag_new_content"
},
"if_duplicate_found": {
"action": "append_to_canonical_ticket",
"canonical_ticket": "oldest_open_ticket_matching_all_criteria",
"write_to_canonical": [
"message_summary",
"new_information_present: true/false",
"sentiment: escalatory|neutral|positive",
"original_channel",
"original_timestamp"
],
"reply_to_customer": "single_confirmation_only",
"forbidden": ["new_ticket_number", "new_confirmation_email"]
},
"if_merge_uncertain": {
"action": "create_linked_ticket",
"linked_to": "suspected_canonical",
"mark_for_human_review": true
}
}
}
Three details in that schema do the heavy lifting:
- The 7-day closed-ticket window. Most duplicate messages arrive after someone closed the first ticket — the customer replies to the resolved email thread. If your dedup only searches open tickets, you'll recreate the same problem one week later.
-
new_information_present. A pure "any update?" duplicate is a touch. A duplicate that says "it's been nine days and now the box is damaged" is a touch plus a new problem. The canonical ticket needs to show which is which, or your agent replies "we're still working on it" to someone who now has a damaged box. -
sentiment: escalatory. The third duplicate touch from one customer is statistically the one where they start writing to your payment processor. Escalatory duplicate touches should jump the canonical ticket's priority, not sit in the pile.
Step 3: One customer, one confirmation
Here's the most embarrassing failure in the eleven-ticket story: the customer got eleven confirmation emails, and zero follow-ups after that. Eleven messages that said "we received your request!" and no messages that said "here's what's happening."
The fix is a hard rule your agent can't override:
A duplicate touch generates exactly one reply, and the reply comes from the canonical ticket.
That one reply should answer three things:
- Where the original ticket stands (stage + owner).
- What happens next and when ("your replacement ships Thursday, tracking arrives by email").
- A plain acknowledgment that you know they've contacted more than once: "I've combined your three messages into one case so nothing gets lost."
Customers don't get angry because they contacted you four times. They get angry because four separate robots acted like it was the first time, every time. Naming the merge — "I've combined your messages into one case" — costs one sentence and converts fury into tolerance almost every time I've watched it happen.
Step 4: The weekly duplicate audit (20 minutes, five checks)
Run this every Monday. It takes twenty minutes with a spreadsheet and it's the only reliable way to know whether your dedup layer is alive or decorative.
| # | Check | Query / method | Healthy threshold |
|---|---|---|---|
| 1 | Same-identity open tickets | Count customers with ≥2 open tickets in last 7 days | < 2% of open tickets |
| 2 | Same-entity collisions | Group open tickets by (customer, order/booking ID); count groups > 1 | 0 groups > 2 tickets |
| 3 | Post-close recreation | Tickets created within 7 days after a closed ticket, same customer + entity | < 5% of new tickets |
| 4 | Multi-confirmation spam | Customers who received ≥2 automated confirmations in 24h | 0 |
| 5 | Merge accuracy sample | Read 10 merged tickets; was the merge correct per the 4-part rule? | ≥ 9/10 correct |
Check 1 is your smoke alarm — it runs in ten seconds and catches everything. Check 3 is the sneaky one: recreation-after-close looks great on dashboards ("low open ticket count!") while being the exact behavior that makes customers feel insane. Check 5 is the only check that measures false merges — merging two genuinely different issues is rarer than duplicates, but far more expensive, because the dropped issue now has no ticket at all.
Step 5: The numbers, so you can pitch this internally
From audits across small support teams (roughly 200–2,000 tickets/month):
- 12–18% of tickets in un-deduped AI deployments are duplicates of an existing thread. That's queue noise: agents triaging the same problem three times.
- Duplicate-heavy accounts churn at meaningfully higher rates. Nobody cancels because of the ticket count; they cancel because the last five interactions each started from zero.
- Merged-thread handling cuts handle time roughly in half per duplicate — the agent reads one timeline instead of reconstructing three, and never answers the same question twice in two threads with two different answers.
- The 7-day closed-window alone typically catches a third of all duplicates, since most customers reply to the closed thread rather than opening a new channel.
You don't need a fancy clustering model to start. Identity match plus entity match plus a seven-day window catches the overwhelming majority of real-world duplicates, because real customers repeat themselves far more than they vary themselves.
The 12-line dedup checklist
Pin this next to your agent config:
- Dedup check runs BEFORE ticket creation, not after.
- Duplicate requires all four: same identity, same entity, unresolved stage, no new money/contact change.
- Search scope includes tickets closed within 7 days.
- Canonical ticket = oldest open ticket matching all criteria.
- Duplicate content is appended with channel + timestamp + new_info flag.
- Escalatory sentiment bumps canonical ticket priority.
- Merging is not linking: different issue + related entity = linked, never merged.
- Exactly one confirmation per duplicate touch, from the canonical ticket.
- Confirmation names the merge: "I've combined your messages into one case."
- Never reply to a duplicate with a new ticket number. Ever.
- Weekly audit: same-identity open tickets < 2%, recreation-after-close < 5%.
- Monthly: sample 10 merges, verify against the four-part rule, fix the prompt drift.
The eleven-ticket customer eventually got her refund — after your competitor's ad showed up in her feed while she waited. The AI that handled her didn't need to be smarter. It needed to know that the second, third, and eleventh message were the same message, and act like it.
Want the full playbook — dedup rules, escalation payloads, audit checklists, and the reply templates that keep merged-thread customers from exploding? Get it here: https://whop.com/agentfast-04b6/
Top comments (0)