Automation · Revenue operations
How to build lead deduplication and merge
Two reps call the same person in the same week because a webinar list arrived after a demo request. The shape of deduplication that catches it, the prompts that build it, and the production detail that decides whether it holds.
Built with Tray Headless
- System Salesforce leads
- Step Scored detection
- Step Survivorship
- Step Move activity
- System Salesforce master
Survivorship is decided field by field, and an owner conflict leaves the workflow rather than being resolved by it.
The short answer
What is lead deduplication?
Deduplication is four parts: detection on a scored rule set instead of an exact email match, field-level survivorship so the merged record keeps the best value from each side, activity and relationship history preserved from both, and an owner rule that never silently reassigns. The mistake that costs most is survivorship. A merge that keeps whichever record is older throws away the enrichment, the corrections and sometimes the phone number that the newer one had.
What matters here
- Exact email match finds the easy duplicates. The expensive ones differ by a plus-address, a nickname or a job change.
- Survivorship is per field, not per record. Keep the newest phone, the human-entered title and the richest firmographics, whichever side they came from.
- Never lose activity. Tasks, emails, campaign membership and opportunities all move to the survivor, or the merge destroys the reason to merge.
- An owner change is a business decision, not a merge side effect. Flag it instead of doing it quietly.
- Merges must be reversible for a window. The first month of any dedupe rollout produces merges somebody will want back.
Who this is for
You run revenue operations or data operations. The CRM has duplicates arriving faster than anybody can clear them, and the last attempt at a bulk merge is still remembered. What you want is detection you can tune and merges you can defend.
How it works in practice
Everything that sits between two records existing and one surviving.
- 1
Detection runs on a scored rule set
Exact email, normalised email, name plus company, name plus phone. Each rule carries a confidence, not a verdict.
- 2
High confidence merges automatically, the rest queue
The middle band goes to a person. Auto-merging a 0.8 match is how you lose a record somebody needed.
- 3
Survivorship is decided field by field
Newest phone, human-entered title, richest firmographics, earliest created date. Not whichever record is older.
- 4
All activity moves to the survivor
Tasks, emails, campaign memberships, opportunities and attachments. Nothing is left pointing at a deleted id.
- 5
Owner changes are flagged, never silent
If the survivor has a different owner, that is surfaced for a decision rather than applied quietly.
- 6
The merge is reversible for thirty days
Both original records are retained, so a bad merge is an undo instead of a reconstruction.
What deduplication is made of
Dedupe is not a matching query. It is four parts, and the second one is where trust is won or lost.
Scored detection
Several rules, each with a confidence: exact email, normalised email, name plus company, name plus phone. Duplicates rarely announce themselves with an identical address.
Field-level survivorship
A rule per field instead of a winning record. The best phone and the best title are frequently on different sides.
History preservation
Activity, campaign membership, opportunities and attachments all move. A merge that loses the history has removed the reason anybody wanted the merge.
An explicit owner rule
Ownership is commercial. The merge surfaces the conflict; a human resolves it.
The Tray Headless prompts
Paste these into Claude Code or Codex with the Tray Headless plugin installed. Each stage runs on its own. The systems named in them are the worked example rather than a requirement, and every prompt says so.
Once per project, run
/tray-workflows:set-workspace
to pick the workspace these build in. Point it at a sandbox first.
- 1
Set up, then learn the real object model
Merging without knowing what points at a record is how history disappears.
Headless skills
build-workflowUse build-workflow. The systems in play are Salesforce, or whatever we run in those seats. I need Lead and Contact fields with types, plus every object that carries a lookup to them: Task, Event, CampaignMember, Opportunity, Attachment, and any custom object. Merging without that list is how activity history disappears.
- 2
Detect on several rules, each with a score
Exact email finds the easy ones. The costly duplicates are not exact.
Headless skills
build-workflowtray-patternsUse build-workflow to build detection. Run these rules, each producing a confidence: Exact match on lower(email) 1.00 Normalised email, plus-addressing and dots stripped 0.95 Same name and same company, normalised 0.85 Same name and same phone in E.164 0.85 Same company and same phone, different name 0.60 Take the highest score per candidate pair. Above 0.90 merges automatically, 0.70 to 0.90 goes to a review queue, below that is not a duplicate. Run it on insert and as a nightly sweep, because duplicates arrive in list imports rather than one at a time.
- 3
Decide survivorship field by field
The part that decides whether people trust the merge.
Do not pick a winning record. Decide per field: Email the one on the record with more activity Phone the most recently modified non-empty value Title human-entered beats enriched, newest beats oldest Company the enriched value if both are present Firmographics the most complete set, not the newest Created date always the earliest, so tenure is not reset Source the earliest, so first-touch attribution survives Show me the resolved record before writing when it goes through the review queue, with each field labelled by which side it came from.
Keeping the earliest created date and source is what stops deduplication quietly destroying first-touch attribution reporting.
- 4
Move every piece of history
A merge that loses activity has removed the reason to merge.
Headless skills
tray-gotchasUse tray-gotchas, then reparent everything to the survivor: Tasks and Events, with their original timestamps intact Campaign memberships, deduplicated so one person is not counted twice in the same campaign Opportunities, and stop the merge entirely if both records have open opportunities, because that is a conversation not an automation Attachments and notes Any custom object with a lookup Verify nothing points at the losing record before it is retired, and keep both originals for thirty days so a bad merge is an undo. - 5
Never reassign an owner quietly
Ownership is commercial, and silent changes destroy trust in the whole system.
If the two records have different owners, do not resolve it in code. Keep the survivor owner, flag the conflict, and notify both owners with the two records and the activity on each. A human decides. Report weekly: duplicates detected by rule, auto-merge rate, review queue size and age, owner conflicts raised, and merges reversed. The reversal count is the honest measure of whether the thresholds are right.
- 6
Validate, then hand it over
So thresholds move on evidence, not by ticket.
Run the per-step schema checks and the whole-workflow audit before this touches production. Start in report-only mode for a fortnight: detect and queue, merge nothing, and read the queue. Then open the same workflow in Tray Build so revenue operations can adjust the thresholds and the survivorship rules in the visual canvas.
What it connects to
Dedupe reads and rewrites the system of record, so it touches everything attached to a person.
Salesforce
Read leads, contacts and everything with a lookup to them. Merge, reparent activity and retire the loser.
Reads and writes
Marketo
Keep the person record aligned, so a merged lead does not remain two people in the marketing database.
Reads and writes
Slack
Raise owner conflicts to the two reps involved, and post the review queue to whoever owns data quality.
Writes
Snowflake
Land every merge with the survivorship decisions, which is what makes a reversal reconstructable.
Writes
Outreach
Suppress sequences for a retired record, so a merged person does not keep receiving email from a dead id.
Writes
Same build, other stacks
The design does not change if you run something else in one of these seats. The same prompts build it against Microsoft Dynamics 365, Google BigQuery, Microsoft Teams, HubSpot, Databricks or Google Chat.
Named systems are the ones most teams run, not the only ones that work. Each is an authentication in your Tray workspace, referenced by name, so the workflow never holds a credential. Where we have a connector page, the name links to it.
Running it in production
Dedupe destroys records by design. That needs more care than most integrations.
The platform runs it, not a laptop under a desk
Nightly sweeps and insert-time detection execute on the same engine, with a full history of every merge performed.
Every merge is reversible for thirty days
Both originals retained with the survivorship decisions recorded. A bad merge is an undo, not a reconstruction.
Managed credentials, not secrets in a config file
Merge permissions delete records by design. That credential belongs in your workspace with a named owner, not in code alongside the logic that uses it.
Ops owns the thresholds
Detection scores and survivorship rules open in Tray Build, tuned by the team watching the review queue.
Start in report-only mode
Detect and queue for a fortnight before merging anything. The first read of the queue always changes the thresholds.
Questions people ask
Why not only match on email?
Because exact email finds the easy duplicates. The expensive ones differ by a plus-address, a nickname, a personal address on a second signup, or a job change at the same company.
What is survivorship?
Deciding which value wins per field, not which record wins. The newest phone and the human-corrected title are frequently on different records, and picking a winning record throws one of them away.
What happens to activity history?
It all moves: tasks, events, campaign membership, opportunities and attachments. If both records have open opportunities the merge stops, because that is a conversation between two reps rather than an automation.
Should a merge change the record owner?
No. Keep the survivor owner, flag the conflict and let a person decide. A silent reassignment is the fastest way to lose the sales team's trust in the CRM.
How do I roll this out safely?
Report-only for a fortnight: detect and queue, merge nothing. Read the queue, move the thresholds, then enable auto-merge above the high band with thirty day reversibility.
Further reading
Background on the same subject, for the case rather than the build.
Related guides
Revenue operations
How to build lead import and validation
Validate before you write, quarantine the bad rows instead of dropping them, dedupe against what is already there, and keep the source. The prompts that build it.
Revenue operations
How to build lead-to-account matching
Domain first, then fuzzy name, then hierarchy, with a confidence score and a review queue for the ones that are close. The Headless prompts that build it.
Revenue operations
How to build lead enrichment that does not stall the queue
Enrich on a budget and a timeout, cache by domain, fall back between providers, and never hold a lead waiting for a field. The Headless prompts that build it.
Revenue operations
How to build a CRM to marketing automation sync
Decide field ownership before you build, sync deltas not everything, handle the person-versus-lead model gap, and break loops. The prompts that build it.
Last reviewed September 2026.