# Building Revenue Cloud Quoting

> Salesforce Revenue Cloud (RCM / RLM) transactional runtime: making a quote line actually resolve a price, subscription and term-defined lines, Apex pricing hooks on RevSignaling, the Context Service read/write surface, decision table refresh, and an index of literal Revenue Cloud error strings. TRIGGER when: a quote or order line prices at zero or will not save; the error mentions Price Book Entry, subscription term, end date, pricing procedure, updatable state, or Invalid tag attribute name key; the user writes or debugs a pricing prehook or posthook, RevSignaling.SignalingApexProcessor, Context.IndustriesContext, queryTags or updateContextAttributes; a PricebookEntry, PriceAdjustmentTier or price was changed and pricing did not move; a term, subscription, evergreen or service contract product is being modelled or priced; a price needs verifying headlessly. DO NOT TRIGGER when: the task is catalog structure, categories, variants or qualification rules (use revenue-cloud-pcm); legacy managed-package CPQ or Bi

- Skill: `mkoeppsf/building-revenue-cloud-quoting` (Agent Skill, multi-file: 12 files)
- Install (CLI): `npx skillmds@latest add mkoeppsf/building-revenue-cloud-quoting`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mkoeppsf/building-revenue-cloud-quoting/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: mkoeppsf (https://skillmd.com/u/mkoeppsf)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/mkoeppsf/building-revenue-cloud-quoting

---


# 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](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

```mermaid
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 `TermDefined` product needs a **model-specific** `PricebookEntry` per selling model
  option (`ProductSellingModelId` populated). A **generic** entry (null
  `ProductSellingModelId`) is what `OneTime` products use. A zero-price generic entry
  left active can win resolution and silently price the line at 0.
- The `ListPrice` step does not read `PricebookEntry` directly. 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](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
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_EXCEPTION` on null.
- `SubscriptionTerm` **or** `EndDate` — enforced by the **pricing procedure**.
- `PeriodBoundary` — enforced by the procedure, as `Proration.StartProrationPeriod`.
- `StartDate` — `Anniversary` measures 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](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`, write `SubscriptionTerm`
- Billing cadence: read `ItemBillingFrequency`, write `BillingFrequency`
- Proration boundary: read `StartProrationPeriod`, write `PeriodBoundary`
- Line start date: read `EffectiveFrom`, write `StartDate`
- Line end date: read `EffectiveTo`, write `EndDate`
- Line discount: read `ItemDiscountPercentage`, write `Discount`
- Line attributes: read `AttributeDefinitionCode` and `AttributeValue`

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](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:

```apex
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](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
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](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](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](references/price-resolution.md) — price book entries,
  selling models, decision table snapshots and refresh
- [references/subscription-lines.md](references/subscription-lines.md) — term and
  evergreen line fields, proration, term-to-date arithmetic
- [references/apex-pricing-hooks.md](references/apex-pricing-hooks.md) — the real
  `RevSignaling` signature, testable gateway pattern, context read/write, procedure
  plan registration
- [references/error-index.md](references/error-index.md) — literal error strings
- [references/what-the-docs-get-wrong.md](references/what-the-docs-get-wrong.md) —
  prescriptions that do not work, verified by deploying them
- [references/verification.md](references/verification.md) — headless pricing
  verification and what stays UI-only
- [references/bundle-and-attribute-gotchas.md](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 polls
  `LastSyncDate` until 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.

