Revenue Cloud Quoting Runtime
Covers the layer between "the catalog is modelled" and "the price came out right".
Scope boundary. For catalog structure, category hierarchy, product variants,
qualification rules and the PCM object model, use the revenue-cloud-pcm skill
instead. This skill deliberately does not repeat that material. What it adds is the
transactional runtime, plus the handful of build-time facts that are commonly
documented wrong.
Everything here was established empirically against a Revenue Cloud org, not read off a datasheet. Where the official references or other skills prescribe something that does not work, that is called out in references/what-the-docs-get-wrong.md — read it before following pricing-hook advice from any other source.
Rule 1: a price comes from a chain, and each link fails with a misleading error
flowchart TD
Product["Product2"]
Option["ProductSellingModelOption<br/>IsDefault = true"]
PBE["PricebookEntry<br/>generic or model-specific"]
Table["Price Book Entry decision table<br/>cached snapshot"]
Price["ListPrice on the line"]
ErrA["Required fields are missing:<br/>[Price Book Entry]"]
ErrB["prices at zero, then the<br/>same [Price Book Entry] error"]
Product --> Option
Option --> PBE
PBE --> Table
Table --> Price
Option -->|"no default option"| ErrA
PBE -->|"no entry for the chosen model"| ErrA
Table -->|"snapshot predates the entry"| ErrB
The error names the last link, so it sends you to the wrong place. Diagnose in chain
order: default selling model option first, then the price book entry, then the table's
LastSyncDate.
Two facts that are load-bearing and hard to find documented:
- A
TermDefinedproduct needs a model-specificPricebookEntryper selling model option (ProductSellingModelIdpopulated). A generic entry (nullProductSellingModelId) is whatOneTimeproducts use. A zero-price generic entry left active can win resolution and silently price the line at 0. - The
ListPricestep does not readPricebookEntrydirectly. It reads a decision table holding a cached snapshot, so a new or edited entry is invisible to pricing until that table is refreshed.
See references/price-resolution.md.
Rule 2: refresh the decision table after any price change
Any change to a PricebookEntry, a PriceAdjustmentTier, or a decision table's source
object has no effect on price until the table is re-synced. There is no way to trigger
this from Apex, so it cannot be bundled into a seed script — it is always a separate
step, and skipping it is the single most likely thing to waste a day.
bash scripts/refresh-decision-tables.sh <org-alias> <TableApiName> [more tables...]
Failure is silent and misdirecting: the line prices at zero and the save reports a missing price book entry.
Rule 3: a term line needs four fields, enforced at two different layers
Any line whose SellingModelType is not OneTime needs all four. Error messages only
ever name some of them, because the two layers report separately:
BillingFrequency— enforced by the record:FIELD_INTEGRITY_EXCEPTIONon null.SubscriptionTermorEndDate— enforced by the pricing procedure.PeriodBoundary— enforced by the procedure, asProration.StartProrationPeriod.StartDate—Anniversarymeasures every period from it.
Neither Product2 nor ProductSellingModel carries a declarative default for any of
them, and the configurator does not collect them. Something has to supply them: a
prehook is the reliable place.
The end date wins. Proration receives both the term and the date range, and the
range takes precedence. SubscriptionTerm = 3 with a one-year end date prices as one
period. Always derive EndDate from the period count so the two agree.
Never write SubscriptionTermUnit, ProductSellingModelId, PricingTerm,
PricingTermUnit or PricingTermCount. All are platform-derived.
See references/subscription-lines.md.
Rule 4: reads use tag titles, writes use attribute names
In a pricing hook these are two different namespaces over the same data. Reading a name
the context does not expose as a tag throws
ContextModificationException: Invalid tag attribute name key. Writing a tag title
where an attribute name belongs is accepted, returns no error, and writes nothing —
which is far worse, because the symptom is a procedure that appears to ignore your value.
- Subscription term: read
ItemSubscriptionTerm, writeSubscriptionTerm - Billing cadence: read
ItemBillingFrequency, writeBillingFrequency - Proration boundary: read
StartProrationPeriod, writePeriodBoundary - Line start date: read
EffectiveFrom, writeStartDate - Line end date: read
EffectiveTo, writeEndDate - Line discount: read
ItemDiscountPercentage, writeDiscount - Line attributes: read
AttributeDefinitionCodeandAttributeValue
Plain StartDate is the header's tag, not the line's. Reading it to test whether a
line has a start date checks the wrong node entirely.
Verify every tag against the org's own context definition before using it. Do not assume a tag exists because a document mentions it.
See references/apex-pricing-hooks.md.
Rule 5: one unwritable node must not fail the save
A hook that throws returns FAILED and rolls back the whole quote save. Deleting a bundle cascades to its components, so a line can be pending deletion while the hook is still writing to it; that write is rejected with Failed to update as data not in updatable state. Guard each per-line write individually and skip the ones that are rejected:
private Boolean tryWriteAttribute(String contextId, List<Object> nodePath,
String name, Object value) {
try {
writeAttribute(contextId, nodePath, name, value);
return true;
} catch (Exception e) {
skippedWrites++;
return false;
}
}
Surface the skipped count in the response message so it stays visible rather than silent.
Do not solve this by querying the $DmlStatus tag, even though the pricing
references prescribe it — see
references/what-the-docs-get-wrong.md.
Rule 6: verify a price headlessly before believing it
Do not conclude that pricing works because a save succeeded. Assert the number:
bash scripts/verify-term-line-pricing.sh <org-alias> <PRODUCT-CODE> \
--cadence Annual --periods 3 --quantity 1
It creates a quote, adds a line without a UnitPrice so the price must come from
the price book, forces a pricing run, and asserts that ListPrice resolved, that
PricingTermCount matches, and that TotalPrice equals unit x quantity x periods.
Omitting UnitPrice is the point. A harness that sets it cannot detect a stale decision
table, because the price never comes from the price book at all.
See references/verification.md for the three-step Place Quote pattern this uses and why lines cannot be added inside the graph.
When something is already broken
Start with references/error-index.md — it maps literal error strings to cause and fix. Match the message text first; the symptom is usually one link away from the actual cause.
Reference files
- references/price-resolution.md — price book entries, selling models, decision table snapshots and refresh
- references/subscription-lines.md — term and evergreen line fields, proration, term-to-date arithmetic
- references/apex-pricing-hooks.md — the real
RevSignalingsignature, testable gateway pattern, context read/write, procedure plan registration - references/error-index.md — literal error strings
- references/what-the-docs-get-wrong.md — prescriptions that do not work, verified by deploying them
- references/verification.md — headless pricing verification and what stays UI-only
- references/bundle-and-attribute-gotchas.md — build-time behaviour that surprises, including attribute-configuring bundles
Scripts
Execute these; they take an org alias and do not carry any project-specific ids.
scripts/refresh-decision-tables.sh— re-syncs named decision tables and pollsLastSyncDateuntil it advances, failing loudly if it does not.scripts/verify-term-line-pricing.sh— end-to-end headless price assertion for a term-defined product.