Why your CRM reporting disagrees with finance
It is almost never the CRM. A join key nobody retired, a table built for one board meeting, a stage that counts quotes as orders. The five causes I find, with the six-figure example that came from one of them.
It is almost never the CRM. When the pipeline number on the dashboard and the revenue number finance signs off disagree, the records in the CRM are usually correct when you check them one account at a time, and the gap comes from a join key, a stale table, a stage definition or a sync, one of five places rather than everywhere.
I spent a stretch of this year inside exactly this problem for a B2B SaaS platform, where leadership could not trust the revenue board and nobody could prove which figure was right. The core data reconciled to the penny on the control accounts I checked. The forecast was still wrong by six figures. Here is where that came from, and the other four places I look.
What was actually wrong?
The forecast panels were keyed on a legacy customer identifier, which had been the way the product linked a customer to their orders until matching moved to the company domain. Nobody retired the old key. It was empty for the large majority of companies, and the forecast joined on it anyway, so any customer who had come in since the change simply did not exist as far as the forecast was concerned.
That dropped dozens of active customers, worth seven figures of orders across their lifetime and a six-figure chunk of the quarter being forecast. Some of the names missing were among the largest accounts on the platform.
Repointing the join onto the domain moved the quarter’s forecast up by an amount that matched the audited exposure almost exactly. A correction, not growth, and it went to the board the same day with the explanation attached.
The point is not the size of the number. It is that the CRM was right the whole time. The customers were in there, with their orders, reconciling to the product source exactly. One join in one reporting layer made them invisible.
The five places I look
| Cause | What it looks like from the outside | How you catch it |
|---|---|---|
| A legacy join key | A total that is too low by a lump, with recent customers missing | Count nulls on every key used in a join. Anything over a few percent null is not a key |
| A snapshot table nobody refreshed | History that stops on one date, or one account wildly under-reported | Check the sync timestamp. If every row shares one, it is a snapshot |
| A stage or lifecycle definition | Accounts promoted too early, pipeline value inflated | Read the workflow that sets the stage and compare its filter to the product’s own definition |
| A sync that drops columns or values | A report that used to work and now shows nothing, or multi-domain companies not matching | Pin the schema explicitly. Autodetect deletes all-null columns without telling you |
| Pace maths on the wrong dates | A quarter that reads far more behind than the team believes | Find out who sets the target date. If the system stamps it, the pacing is fiction |
All five turned up in the same instance, which is normal. They accumulate, because each one was reasonable at the time and nobody was paid to go back.
The table built for one board meeting
The second biggest gap came from a quarterly revenue table that turned out to have been built once, on a single date, as a workaround to get a number for a board pack. Every row shared one sync timestamp. It was never refreshed, it was set to delete itself a couple of months later, and two live panels were still reading history off it.
Even within its own snapshot it was incomplete. One quarter showed a fraction of the revenue its own order count implied, and one large account showed a few thousand in the table against seven figures live. Anyone reading a trend off that table was reading a trend in when it had been built.
The fix was a small scheduled job that aggregates orders by company and quarter from the product database directly, keyed on domain, into one table in the same dataset the dashboard already reads from. No new database, no new entity. Validated against the business’s own historic reporting, which agreed to within a fraction of a percent once the quarter boundaries were aligned on the same date field.
When a quote counts as an order
The lifecycle stages were promoting accounts on order count, which sounds right until you find that the count included quote requests. One account had a handful of order records in the product, fewer than half of them real orders and the rest old quotes, and it was staged as a top-tier customer on the strength of all of them.
The attributes were fine. The stage definition was wrong. The product’s own order model defines a real order as a specific set of statuses, ordered through delivered, and excludes drafts, quotes, holds, cancellations and declines. The stage workflow was not using that set. Pointing it at the product’s own definition fixed the promotions, and it also exposed that the value tiers were labelled as if they were annual when the logic underneath was all-time cumulative spend.
The general lesson: when a CRM stage disagrees with what finance would call a customer, read the workflow that sets the stage. It is usually years old and was written before the definition existed.
The pacing number that frightened everyone
The quarter first read as more than half behind target. It was actually about a quarter behind, which is a different conversation to have with a board.
Two things produced the gap. Target dates on open opportunities were being stamped automatically by the system rather than set by the reps, so the pacing calculation was comparing progress against dates nobody had chosen. And the pace maths pro-rated open opportunities by calendar time in a way that assumed linear delivery, which for a business with lumpy orders is not how the quarter arrives. Fixing both, with the founder in the room for the target date decision, brought the number to something the team recognised.
What good looks like afterwards
The rule that came out of it is simple to state. Reporting reads product truth end to end. Every revenue and forecast number on the dashboard comes from the product’s own order data through the reporting layer, and the CRM is fed from the same source, so a bug in the push to the CRM can never become a bug in a board number.
Underneath that, four habits keep it honest. Control accounts reconciled by hand and rechecked after every change. A pinned schema on every sync so columns cannot vanish. A daily drift check that compares the CRM to the product and reports a match rate, so regressions are detected rather than rediscovered in a board meeting. And every definition, real order, active customer, quarter boundary, written down once and used everywhere.
None of this required a new tool. It required someone to read the joins.
If your dashboard and your finance team disagree and nobody can say why, that reconciliation is the first thing I do in a CRM and data audit.