Matching customer records without a shared ID
An automation should link a record across two systems only on exact normalized keys: a stored cross-reference ID, a lowercased email, or a phone in E.164 format. Everything that merely looks similar goes to a human review queue. The reason is asymmetry, not accuracy: HubSpot states that records cannot be unmerged, so a false merge is permanent, while a false split costs one manual merge.
Two systems, no common ID, and an automation that has to decide whether the person in this payload is already in your CRM. Match only on exact normalized keys, and send anything that merely looks similar to a person. The reason is not accuracy. It is recovery: HubSpot's documentation states that it is not possible to unmerge records, so a wrong match is permanent, while a missed match costs someone one manual merge.
The two ways to be wrong are not equally expensive
Every guide to this problem ranks matching techniques by accuracy and lands in the same place, a similarity score with a threshold you tune. That framing hides the decision that actually matters. There are two ways to be wrong and they cost wildly different amounts.
A false split is one person becoming two records. Someone spots the duplicate in a list view, merges it, and the whole incident is over in under a minute.
A false merge is two people becoming one record. What that costs depends entirely on where it happened.
| System | What a merge does to the losing record | Can you get it back |
|---|---|---|
| HubSpot | The primary record's values win. Where the primary is empty, the secondary's value fills in. The secondary email survives as an additional email on the merged record. | No. HubSpot states it is not possible to unmerge records. The documented workaround is to take that leftover secondary email or domain and create a brand new record, with none of the original history, associations, or timeline. |
| Salesforce | The non-master contacts move to the Recycle Bin. Related items from them reassociate to the master, and the merge date becomes the master's Last Modified date. | Partly. You can restore the non-master shell from the Recycle Bin. Field values that the merge overwrote on the master are not restored. |
| Airtable, Google Sheets, a plain table | Nothing formal happens, because there is no merge concept. "Merge" means your automation's update overwrote fields on one row. | Only from a backup or a snapshot taken before the run. |
There is a second cost that does not show up in a data quality dashboard. A false merge puts one real person's email and phone number onto another real person's record. Every export, every campaign, and every support view after that shows the wrong contact details, and if a deletion request later runs through that record you delete the wrong customer's data. We wrote about the mechanics of that in deleting customer data from an AI system.
So the design rule falls out of the recovery column, not out of a benchmark. An automation may decide on its own only where a wrong answer is cheap to reverse. Everywhere else it proposes and a person disposes.
Your Find step already picked a winner
Before building any ladder, kill the silent decision that is probably already running.
Zapier's documentation on search steps is explicit: "The Zap will only return the first search result." So a search for a contact named John Smith that matches three records returns one of them, the step turns green, and the next step writes an order to whichever record happened to sort first. Nothing in the run history says a choice was made.
Search steps fail loudly on zero results. The run halts, and you notice. They do not fail on ambiguity, and ambiguity is where the damage lives. Pair that behavior with the "create a record if it doesn't exist" checkbox and one configuration produces two opposite failures: search too narrowly and you create a duplicate, search too broadly and you attach a stranger's order to a real customer.
The fix is to make the search return a count instead of a record.
- In Zapier, switch the search to "return all results as line items" and branch on how many came back.
- In n8n, a search node returns every matching item by default, so read the item count before any write step.
Then branch on that count. Zero results creates. Exactly one result links. Two or more stops and queues. Position in a result set is not identity, and no platform will tell you it made that call on your behalf.
Normalize before you compare, and know which rules are per domain
Comparing raw strings is what manufactures false splits. Normalize both sides into a comparison key, store the key in its own field, and never overwrite what the customer actually typed.
Email. Lowercase both sides first, since that alone resolves a large share of near-misses. Then dots, which are where people get this wrong in an interesting way. Google's Gmail help states that johnsmith@gmail.com and j.o.h.n.s.m.i.t.h@gmail.com are the same address and land in one inbox, so stripping dots from the local part is correct there. The same page also states that if the account is Gmail through work or school on a custom domain, dots do change the address. Dot stripping is a per-domain rule, not a global one. Apply it to gmail.com and googlemail.com. Apply it to your client's own domain and you will collapse two distinct mailboxes into one key, with no way afterward to tell which contact belonged to which person.
Phone. Normalize to E.164, the ITU numbering plan that caps a number at 15 digits including a country code of one to three digits. "(781) 303-7979", "781-303-7979", "1-781-303-7979", and "+17813037979" are one phone and four different match keys. Pick a default country for numbers that arrive without one, and write the normalized value to a separate field so the display value stays readable for the humans who have to call it.
Company. Match on the email domain, not the company name. "Acme Inc", "Acme, Inc." and "ACME Incorporated" are three strings. acme.com is one. Take the domain off the contact's work email and use that as the company key, then drop the free-mail domains from the key entirely: gmail.com, outlook.com, yahoo.com, icloud.com, hotmail.com. Skip that step and every consumer customer joins one enormous company record. A B2B CRM with a single account holding four thousand unrelated contacts got there exactly this way.
The match ladder
This is the part that replaces the threshold. Three tiers, and the tier decides who is allowed to act, not how confident anyone feels.
Tier 1, exact and sufficient alone:
- A stored cross-reference ID, meaning you already wrote system B's ID into system A or into a mapping table.
- Normalized email, exact string equality after normalization.
- Normalized phone in E.164, exact.
Tier 2, exact but sufficient only in pairs:
- Company domain plus normalized last name.
- Company domain plus normalized company name.
- Normalized street address plus normalized last name.
Tier 3, never sufficient:
- Name similarity, address similarity, or any score out of 100 from a fuzzy matching node.
The rules that govern them:
- One tier-1 hit links the records. Stop searching.
- Two tier-2 hits that agree on the same record link it. A single tier-2 hit goes to the queue.
- Anything reachable only through tier 3 goes to the queue, always, regardless of how high the score is. Tier 3 exists to find candidates for a human, not to authorize a write.
- Zero hits creates a new record.
- Two tier-1 hits that disagree go to the queue. If the email matches record A and the phone matches record B, that is not a matching problem. Either you already have a duplicate, or two people share a household phone. Picking one turns an existing, fixable duplicate into a permanent merge.
That last rule is the one most builds skip, and it is the one that saves you. The conflicting-key case is rare enough that people never plan for it and common enough that it eventually happens to every sync.
Then the step that makes all of this a one-time cost. The moment a match resolves, whether the ladder did it or a person did, write each system's ID into the other, or into a mapping table with three columns: system A ID, system B ID, date resolved. Every run after that is a tier-1 lookup. If your workflow is running fuzzy comparison logic on every single event, the matching is not the bug, the missing mapping table is. The same principle keeps a two-way sync honest, which we covered in two-way sync keeps overwriting your good data.
Run it in read-only mode for a week first
Build the ladder, point it at live data, and have it write what it would have done to a spreadsheet instead of to the CRM. Four columns are enough: incoming record, matched record, tier that hit, action it would have taken.
After a week, read the tier-3 rows and the conflict rows. That count is your review queue volume, and it tells you what to do next better than any accuracy metric. Under roughly twenty rows a day, one person clears the queue in ten minutes each morning and the design works. Over a hundred a day, the problem is upstream capture, not matching, and no ladder will fix it. Go fix the form that lets people type a company name freehand with no email required.
Only after that week do you let it write. Nothing in the read-only phase is irreversible, which is the entire argument. This ladder is standard equipment in the sync and lead-routing work we ship through workflow automation systems and customer acquisition systems, and the review queue is never the embarrassing part of the build. It is the part that lets the rest of it run unattended. If you are staring at a CRM that already has the damage in it, tell us what the two systems are and we will tell you which tier your data can actually support.
Frequently Asked Questions
SOURCES & CITATIONS
- Merge records — HubSpothttps://knowledge.hubspot.com/articles/kcs_article/contacts/can-i-unmerge-contacts
- Considerations for Merging Duplicate Contacts — Salesforcehttps://help.salesforce.com/s/articleView?id=sales.contacts_considerations_for_merging_duplicates.htm&language=en_US&type=5
- Search for existing data in Zaps — Zapierhttps://help.zapier.com/hc/en-us/articles/8496241402253-Search-for-existing-data-in-Zaps
- Dots don't matter in Gmail addresses — Googlehttps://support.google.com/mail/answer/7436150
About Alexey Yushkin
Alexey is the founder of GENERAL INFORMATICS LLC. He designs and ships AI and automation systems for businesses and operators across the US.
Related reading
Want this kind of system in your business?
We build practical AI and automation systems for operators. Send us your current workflow and we will show you what to automate first.
