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.
| Process | Stages | Record types |
|---|---|---|
| Bid | Discovery → Working - Bid → Delivered - Bid → Closed Won / Lost | New Construction |
| Transactional | Quoted → Closed Won / Closed Lost | Service, 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.