GTM Systems
The Integration Dropped 1,400 Records and We Found Out Ninety Days Later
A sync silently failed on a subset of records and nobody noticed until the quarter did not add up. The fix is reconciliation built into every integration so a dropped record surfaces the same day, not next quarter. Here is the pattern and the queries.
· 8 min read
The quarter did not add up, and it took a week to figure out why. The billing system showed 1,400 more active subscriptions than Salesforce did. The integration that synced subscriptions into the CRM had a green light on every dashboard, no errors in the logs, no failed jobs. It had been quietly skipping records for about ninety days because a required picklist value on the CRM side had been renamed, so any record carrying the old value hit an unhandled mapping case and got dropped without an error. The integration reported success because from its own point of view it had processed every record it chose to process. The 1,400 it skipped were never in its count.
That is the failure mode of an integration with no reconciliation: it can only tell you about the records it knows it touched. It cannot tell you about the ones that fell out of the pipe, because from inside the pipe they were never there. A green dashboard means “the job ran,” not “the data is correct.” The fix is to build reconciliation as a first-class part of every integration, so the question “does the source still equal the destination” gets asked automatically, every day, and a divergence pages someone the morning it happens instead of surfacing when the quarter fails to close.
A job status is not a data check
Every integration platform reports job status, and every team mistakes it for correctness. Job status answers “did the run complete without throwing,” which is a different question from “does the destination now match the source.” The dropped records did not throw. A record with an unmapped picklist value that gets silently skipped, a batch that times out and gets marked partial-success, an upsert that matches on the wrong external id and updates the wrong record, all of these leave the job green and the data wrong. The only way to catch them is to compare the two systems by count and by value, independent of what the job says about itself. Reconciliation is that independent comparison.
View as table
| Point | Value |
|---|---|
| Wk1 | 8,200 |
| Wk2 | 8,350 |
| Wk4 | 8,500 |
| Wk7 | 8,480 |
| Wk10 | 8,460 |
| Wk13 | 8,450 |
Three levels of reconciliation, cheapest first
Reconciliation is not one thing; it is a ladder you climb as the data matters more. The cheapest check is a count reconciliation: does the source have the same number of records as the destination, filtered to the same scope? It catches wholesale drops like the 1,400 and costs one query on each side. Above that is a checksum or hash reconciliation: sum a key numeric field (total ARR, count by status) on both sides and compare, which catches value drift that a count misses. The most thorough is a row-level reconciliation: pull the set of external ids from both systems and diff them, so you get the exact records that are missing or extra, not just that a gap exists. Run the count check every sync, the checksum daily, and the row-level diff on demand when the cheaper checks flag a divergence.
The count check that would have caught it in a day
The cheapest rung is a scheduled job that counts the source and the destination on the same scope and alerts when they diverge beyond a tolerance. In Salesforce this is a scheduled Apex or Flow that queries the CRM side, calls the source system for its count, and posts to Slack or creates a case when the delta exceeds a threshold. The whole thing is a few lines.
// Scheduled daily. Compares CRM active-subscription count to the billing
// source count and raises an alert when they drift past tolerance.
Integer crmCount = [
SELECT COUNT() FROM Subscription__c WHERE Status__c = 'Active'
];
Integer sourceCount = BillingApi.activeSubscriptionCount(); // callout
Integer delta = Math.abs(crmCount - sourceCount);
if (delta > RECON_TOLERANCE) {
Recon_Alert__e evt = new Recon_Alert__e(
Object__c = 'Subscription__c',
Crm_Count__c = crmCount,
Source_Count__c = sourceCount,
Delta__c = delta
);
EventBus.publish(evt); // platform event → Slack / case / on-call
}
The worked example: what the row-level diff finally showed
Trust the green light versus verify the data
| No reconciliation | Reconciliation built in | |
|---|---|---|
| What "healthy" means | The job ran without throwing | The counts and checksums match |
| A silently dropped record | Invisible until the numbers fail | Flagged the day it happens |
| Time to detection | ~90 days, at quarter close | Same day, by the daily check |
| Finding which records | A week of forensics | One row-level diff |
| Blast radius | 1,400 records, a missed quarter | A Slack alert and a mapping fix |
| Who finds it | Finance, angry, at close | On-call, calm, next morning |
Here is how I build reconciliation into every integration
- 1
Define the scope both sides can agree on
The exact filter that should produce identical sets: active subscriptions, open opportunities, contacts created since a date. Reconciliation is only meaningful against a shared definition of what should match.
- 2
Add a count check to every sync
Cheapest rung. After each run, count source and destination on the shared scope and compare. This alone catches the wholesale drops that leave logs green. Ship it with the integration, not later.
- 3
Add a daily checksum on the numbers that matter
Sum ARR, count by status, whatever the business reconciles on. A count can match while values drift; the checksum catches that. Run it on a schedule independent of the sync.
- 4
Keep a row-level diff ready
When count or checksum flags a gap, pull external ids from both sides and diff the sets to name the exact records. Build it once so it is a query away, not a fire drill.
- 5
Alert to a human, not a log
A divergence past tolerance posts to Slack or creates a case with the counts and the delta. A green dashboard nobody reads is not detection; an alert that reaches on-call is.
- 6
Treat every silent-skip path as a bug
Any code that swallows an error and continues is a reconciliation gap waiting to happen. Fix the skip to log and alert, and let reconciliation be the backstop for the ones you miss.
The integration was never broken in the way anyone was watching for. It ran, it succeeded, it reported green, and it silently dropped 1,400 records for a quarter because a picklist got renamed and nobody told the mapping table. A job status tells you the pipe ran; only reconciliation tells you the same water came out the other end. Build the count check into every sync from day one, add a daily checksum, keep the row-level diff a query away, and route divergence to a human who is awake. Then the next silent drop is a Slack message on a Tuesday morning, not a missed quarter and a week of forensics. The reconciliation instinct is the same one behind designing CPQ that ties out to finance: trust the numbers only when two independent systems agree, and check that they agree on purpose rather than by luck.
Keep reading
One email. Every week.
One email a week: a systems problem I architected or untangled, with the schema, the config, 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