Putting the org in git found a forecasting bug nobody had noticed

Three Opportunity stages carried forecastCategory=Closed, so open pipeline was sitting in the Closed bucket. Nobody spotted it in the UI. The metadata retrieve showed it in one line.

I was not looking for a bug. I was retrieving the Opportunity stage set so I could design two sales processes on top of it, and the answer was sitting in the file.

Three stages — Discovery, Working - Bid and Delivered - Bid — all carried forecastCategory=Closed.

Every one of them is an open stage. Open pipeline had been sitting in the Closed forecast bucket, and nobody had noticed.

Why nobody noticed

This is the part worth dwelling on, because it explains a whole category of Salesforce problems.

A forecast category is not visible where people work. The rep sees the stage name on the record and on the path. The manager sees the stage on the list view. Nothing in the daily interface says “and this one counts as closed.” You find it by opening each stage in Setup, one at a time, and reading a dropdown — which is a thing precisely nobody does for fun.

The numbers looked plausible too, which is the real killer. A forecast that reads wildly wrong gets investigated. A forecast that reads slightly optimistic gets explained away.

What the retrieve showed

In source, the whole stage set is one file. Twelve lines of forecastCategory in a column, where a pattern is not something you have to hold in your head across twelve screens.

force-app/main/default/standardValueSets/OpportunityStage.standardValueSet-meta.xml

<!-- before: three open stages mapped to Closed -->
<standardValue>
    <fullName>Discovery</fullName>
    <closed>false</closed>
    <forecastCategory>Closed</forecastCategory>
    <probability>10</probability>
</standardValue>
<standardValue>
    <fullName>Working - Bid</fullName>
    <closed>false</closed>
    <forecastCategory>Closed</forecastCategory>
    <probability>25</probability>
</standardValue>

Note <closed>false</closed> sitting directly above <forecastCategory>Closed</forecastCategory> in the same element. The stage knows it is open. The forecast category says otherwise. Two adjacent lines contradicting each other is the kind of thing text review catches instantly and a settings screen hides completely.

The fix was mechanical once seen: Discovery and Working - Bid to Pipeline, Delivered - Bid to Best Case — a bid that has been delivered to a builder is a real commitment but not a signature. A Quoted stage was added for the transactional path at Forecast, 60%.

The design that surfaced it

The retrieve happened because of a design question: how many sales processes does a company with three lines of business need?

The obvious answer is three — one per line of business. The answer we landed on is two.

ProcessStagesRecord types
BidDiscovery → Working - Bid → Delivered - Bid → Closed Won / LostNew Construction
TransactionalQuoted → Closed Won / Closed LostService, Wholesale

Three record types, two processes. Service and Wholesale are different businesses with different customers and different fulfilment, but the shape of the sale is identical: quote it, win or lose it. Giving them separate processes would have been three stage sets to maintain to express one difference that is not a stage difference at all.

Commercial is not on this list, which surprises people. A commercial bid runs the New Construction motion and commercial break/fix runs the Service one — so commercial is a property of the customer, carried on the Account, not a kind of Opportunity.

The deploy error on the way out

One more, because it cost a cycle and the message is unhelpful.

Permission set files have to be schema-ordered: description, fieldPermissions, hasActivationRequired, label, recordTypeVisibilities. The fix is to rebuild the file in order and re-emit it sorted rather than appending to it — which is now how that file gets written every time, because appending is exactly what caused this.

The argument this is really about

Doorvana’s org had been running for a while before I touched it. The forecast bug was not introduced by anybody careless; a stage got created by copying another one, the category came along for the ride, and there was no moment afterwards where a human would naturally have looked at it.

That is the case for metadata in source control, and it is not the case people usually make. The usual pitch is rollback and review — real, but abstract. The concrete version is that a retrieve turns your configuration into something a person can read, and reading it finds things that using it never will.

This one had been quietly misreporting pipeline. It cost one line to fix and would have cost nothing to keep missing, which is the problem with it.

Filed under

Disclosure: Doorvana's owner is also a co-owner of Nocona Consulting. This is a real engagement between two separate companies, but not an arm's-length one — and not an independent reference. More on the work page.

FAQ

What does forecastCategory do on a Salesforce opportunity stage?

Every Opportunity stage maps to a forecast category — Pipeline, Best Case, Commit, Closed or Omitted — and that mapping, not the stage name, is what forecasting and most standard reporting actually read. A stage called Discovery that is mapped to Closed will be counted as closed business by the forecast regardless of how obviously open it looks in the UI.

Why would open pipeline show up as closed in a Salesforce forecast?

The most common cause is a stage whose forecast category was set incorrectly when the stage was created, usually by copying an existing stage. The stage name looks right on the record and in the sales path, so the error is invisible everywhere except the stage configuration itself. Reviewing the stage-to-category mapping as a list is the fastest way to catch it.

How does source control help find Salesforce configuration bugs?

A metadata retrieve renders configuration as text, which makes it reviewable in ways a settings screen is not. Values that require clicking into each record individually in Setup appear side by side in one file, so an inconsistency that is invisible across twelve screens is obvious in twelve adjacent lines. Diffing also makes every later change explicit rather than something someone remembers doing.

Should each line of business get its own Salesforce sales process?

Only if the stages genuinely differ. Several lines of business often share a stage set and differ only in who works them, in which case one sales process and a field is enough. Doorvana ended up with two sales processes across three record types: New Construction runs a bid process, while Service and Wholesale share a shorter transactional one.

Working on something like this?

I can tell you what it takes.

If you run a home services business and this looks like a problem you have, book a call and we will talk through it.