Article
The Tombstone Trap: Why CRM Merges Resurrect Zombie Records
Most GTM engineers treat a CRM record ID as a permanent, immutable anchor. We build our Postgres tables, webhook listeners, and Segment schemas around the logic that 0015f00000Gv789 is the one true source for a specific account.
This assumption is a liability. In any CRM used by an actual sales team, deduplication is a constant, messy background process. When two records merge, one survives and the other becomes a "tombstone"—a soft-deleted record that points to a new canonical ID.
If your sync architecture isn't built to resolve these tombstones, it will eventually resurrect them. You’ll end up with "zombie" records in your downstream systems that trigger duplicate outreach, break attribution, and turn your billing data into a crime scene.
The Mechanics of the Soft Delete
To build a resilient pipeline, you have to look at how CRMs handle merges under the hood. They don't just delete the losing record; they create a lineage that standard API calls are designed to ignore.
Salesforce: The MasterRecordId
In Salesforce, merging Account A into Account B doesn't purge Account A. Instead, Account A is soft-deleted: its IsDeleted flag is set to true, and the MasterRecordId field is populated with Account B's ID.
The trap is in the query behavior. A standard SOQL query—SELECT Id, Name FROM Account—will never return Account A. To see the lineage, you must explicitly use the queryAll() API or append ALL ROWS to your queries.
-- The only way to find where your old records went
SELECT Id, MasterRecordId, IsDeleted
FROM Account
WHERE IsDeleted = true
AND MasterRecordId != null
ALL ROWS
If your ingestion pipeline only polls for updated records where IsDeleted = false, you are effectively blind to the fact that an ID you currently track has been deprecated and remapped.
HubSpot: The Event-Based Merge
HubSpot handles this through its Webhooks API, but the logic is event-driven rather than state-driven. When a merge occurs, HubSpot fires a contact.merge or company.merge event.
The payload identifies the primaryObjectId (the winner) and the mergedObjectId (the loser). Unlike Salesforce, where you can query the tombstone state at any time, HubSpot requires you to catch the event or specifically inspect the merged-vids in the identity profiles of a contact. If your listener only processes propertyChange events, you’ll miss the merge entirely, leaving two disconnected records in your warehouse that HubSpot now considers one.
How Zombies are Resurrected
A zombie record occurs when a downstream system attempts to "fix" a CRM record it thinks is missing.
Consider a typical bi-directional sync failure:
- Your warehouse has a record for
Lead_123. - In the CRM, a rep merges
Lead_123(loser) intoLead_456(winner). - Later, an enrichment tool updates
Lead_123in your warehouse. - Your sync tool attempts to push that update to the CRM using the
Lead_123ID. - The CRM API returns a
404 Not Foundbecause the record is soft-deleted. - A naive sync treats that
404as an accidental deletion and re-creates the record in the CRM to "fix" the state.
You have now successfully resurrected a duplicate that a human spent time cleaning up.
Building an Alias Resolution Layer
To solve this, GTM engineers should implement an explicit Alias Resolution Layer. This is a persistent mapping table that acts as a traffic controller for every ID entering or leaving your systems.
The Schema:
| legacy_id | canonical_id | source_system | discovered_at |
|---|---|---|---|
| 0015...A | 0015...B | salesforce | 2024-05-10 09:00 |
| vid_123 | vid_456 | hubspot | 2024-05-10 09:15 |
The Implementation Logic: Every time your ingestion engine receives an ID—whether from a webhook, a bulk export, or a message queue—it must pass through an alias check:
- Check the Alias Cache: Is the incoming ID in the
legacy_idcolumn? - If Yes: Replace the incoming ID with the
canonical_id. Route the update to the surviving record. - If No: Proceed as normal. However, if the CRM returns a
404, trigger a specific "Tombstone Check" (e.g.,queryAllin Salesforce) before assuming the record is truly gone.
The Engineering Trade-offs
Maintaining identity lineage isn't a free lunch. There are three operational realities you have to manage:
1. The Latency Tax Adding an alias lookup to every sync operation introduces a database read. In high-throughput pipelines, this can become a bottleneck. We solve this by keeping the alias table in a fast key-value store like Redis. The write happens when the merge event is detected; the read happens on every inbound update.
2. Circular Merges and Chains
Records are often merged multiple times. A is merged into B, then B is merged into C. Your resolution logic must be recursive, or your ingestion process should "flatten" the table so that A points directly to C. Without flattening, your sync will eventually fail when it tries to update a record that is itself a tombstone.
3. The CDP Fallacy Many CDPs claim to handle identity resolution out of the box. While they are great at mapping an email address to a cookie ID, they rarely handle the internal object-level merges of a CRM. If you send a Salesforce delete event to a CDP, it will often just drop the record. You still need to manage the CRM-specific lineage at the ingestion layer to ensure that historical data attached to the old ID is correctly merged into the new one.
Operational Reality
You cannot solve this problem by restricting merge permissions in the CRM UI. Merging is a sign of a healthy, active sales team cleaning up their data.
If your code assumes a 404 from an API means a record should be re-created—or if you ignore IsDeleted records in your exports—you are sitting on a tombstone trap. High-integrity GTM systems don't just track what currently exists; they maintain a map of what used to be.
— C.B.