SfSkills Domains Docs GitHub

Home / omnistudio / calculation-procedure-design

calculation-procedure-design

Design Calculation Procedures and Matrices for pricing, rating, and rules-heavy scoring. Triggers: calculation procedure, calculation matrix, expression set, decision matrix. NOT for Salesforce CPQ price rules — use admin/cpq-pricing-rules. NOT for Flow decision logic — use flow/flow-decision-element-patterns.

Open the skill folder on GitHub


Calculation Procedure Design

Use this skill when designing a pricing, rating, scoring, or eligibility engine on OmniStudio's Business Rules Engine — and when a procedure that "looks activated" is returning null. It covers the matrix column model, the version selection rule, the invocation surfaces, and the handful of field-level facts that decide whether the design works.


The Names, Reconciled

Three vocabularies describe two artifacts. You cannot look up a field under the designer's name, so start here.

Designer labelAPI objectVersion objectSince
Calculation ProcedureExpressionSetExpressionSetVersionAPI 55.0
Calculation MatrixCalculationMatrix — the object reference labels it Decision MatrixCalculationMatrixVersionAPI 53.0
—CalculationMatrixColumn—API 53.0

An expression set is, verbatim, "a series of steps connected in a logical flow built from variables, constants, conditions, calculations, lookups, and aggregations."

Two supporting objects worth knowing exist: ExpressionSetView exposes file-based expression sets, which are "read-only templates that must be cloned before modification"; ExpsSetObjectAliasFieldVw lists the source objects, aliases, and fields an expression set uses, "for permission verification purposes."


The Column Model Is Where Designs Go Wrong

Two facts about CalculationMatrixColumn invalidate the way most people first draw a matrix.

Wildcards are a column opt-in, not a * in a cell

FieldTypeDocumented meaning
IsWildcardColumnboolean, default false"Specifies that this column can contain a wildcard value such as ALL."
WildcardColumnValuestring"The value that indicates a wildcard, for example ALL."

The token is whatever you set WildcardColumnValue to, on each column where IsWildcardColumn is true. A column that has not opted in treats * as a literal string no real input will ever equal — so the fallback row is dead and fails silently. Being per-column is useful: permit a wildcard on Region while requiring an exact ProductTier.

Ranges are a DataType, not a Min/Max column pair

DataType is a restricted picklist with exactly these values:

The seven DataType values are enumerated, with what each one changes about matching, in 1.

NumberRange and TextRange are first-class range types; boundaries go in RangeValues — "a list of values that define range boundaries." One column per dimension, bands that cannot overlap or gap by construction, and the comparison out of your procedure steps. A column-name pair differing only by a Min/Max suffix is the signature of a hand-rolled range that should be one range column.

ColumnType is a restricted picklist: Input or Output. DisplaySequence orders them.


Version Selection Is Three Conditions, Not One

Both version objects carry the same four fields and select the same way.

  1. The activation boolean is true.
  2. The invocation time falls inside [StartDateTime, EndDateTime].
  3. Among the survivors, the highest Rank wins.

Verbatim, from CalculationMatrixVersion.Rank:

Overlapping enabled version windows are resolved deterministically by Rank, not left undefined — quoted from the Object Reference in 1.

The activation field has a different name on each object:

ObjectFieldNote
ExpressionSetVersionIsActivethere is no Status field on this object
CalculationMatrixVersionIsEnabledthere is no IsActive field on this object

The UI calls both "activate." The API does not. Any script that generalises from one to the other reads a field that does not exist.

Use dates for the plan, Rank for the override. A scheduled rate change gets non-overlapping dates. A correction to a live version gets a new version with the same StartDateTime and a higher Rank — which leaves the prior version enabled and auditable instead of editing history. Leave rank gaps (10, 20, 30) so a correction can slot in without renumbering. Two enabled versions sharing a rank in an overlapping window have no documented tiebreaker.


Invoking It

SurfaceDetail
Connect REST APIPOST /services/data/v67.0/connect/business-rules/expressionSet/${expressionSetName}, API 55.0+. Body: inputs (Map<String, Object>[], required) and optional options — effectiveDate, useDatesOnly, actionContextCode, explainabilitySpecName. Returns a Business Rules Result.
Flowthe "Invoke an active expression set" invocable action
Integration Procedurethe Expression Set action ("Invokes Expression Sets and returns results") and the Decision Matrix action ("Calls Decision Matrices with specified inputs")
Apexvia the invocable action, or the Connect REST resource over a Named Credential

options.effectiveDate (ISO 8601) is what makes back-dated calculation correct — version selection evaluates against it instead of "now," so a reissued quote prices at the rates in force on its original issue date. Decide this early; retrofitting it means every caller changes.

Two request-body conventions: for field aliases, append Id to the object alias and pass the source object ID; for context definitions, append Id to the developer name and pass the context ID.

There is no ConnectApi.EvaluationService class and no executeExpression() method. See references/llm-anti-patterns.md §4.


  1. Write the rule as a table and check it is genuinely tabular. If the branch

    conditions are business values (regions, tiers, bands), it is a matrix. If it needs iteration, recursion, or a mid-computation callout, it is Apex.

  2. Design the columns before the rows: ColumnType (Input/Output), DataType

    — using NumberRange/TextRange with declared RangeValues for any banded dimension — and IsWildcardColumn + WildcardColumnValue on every column that needs a fallback.

  3. Decide MatrixType. Grouped with GroupKey/SubGroupKey when the same

    rate structure repeats per partition and each partition changes on its own schedule; Standard when the dimensions are independent lookup inputs.

  4. Build the procedure: matrix lookup steps plus the arithmetic that combines

    them. Keep the step count low — logic that grows past readability belongs in a sub-expression-set or in Apex.

  5. Load rows and gate enablement on LoadProcessStatus = 'Completed', not

    on the load finishing — CompletedWithErrors means a partial load. Assert the CalculationMatrixRow count against the source CSV, then activate leaves before roots (matrices and sub-expression-sets before the procedures that reference them) with distinct, gapped Rank values and explicit StartDateTime / EndDateTime.

  6. Run fixtures from source control — one case per band boundary, one that must

    hit the wildcard, one single-element collection for any aggregation step — and record DecimalScale and ExecutionScale alongside the expected values.

  7. If the procedure sits on a hot UI path, wrap the call in a cached

    Integration Procedure (see omnistudio/integration-procedure-cacheable-patterns). Nothing memoizes procedure results for you.


Review Checklist


Worked Examples (see references/examples.md)

Common Gotchas (see references/gotchas.md)

Top LLM Anti-Patterns (full list in references/llm-anti-patterns.md)


Official Sources Used

See references/well-architected.md for the full source list with the specific claim each source grounds.

Dependencies

Package files