# Approval Rules: predicates and requesters A rule that triggers approval on `totalPrice.centAmount > 5000000` looks correct until Atlas Corporate places an order in New Zealand dollars and the same number means a different amount of money. Approval Rules make governance precise, but a small predicate mistake can make a rule fire on the wrong orders or fail to fire at all. ## The Approval Rule resource An [Approval Rule](/urn?urn=ctp%3Aapi%3Atype%3AApprovalRule) belongs to one Business Unit and combines four governance decisions: whether the rule is active, which Order Predicate it evaluates, which requester roles it governs, and which approver hierarchy must be satisfied. For the full creation payload, see [ApprovalRuleDraft](/urn?urn=ctp%3Aapi%3Atype%3AApprovalRuleDraft). You create a rule through the `as-associate` endpoint, scoped to the Business Unit, that you have used throughout this path. ```http POST /{projectKey}/as-associate/{associateId}/in-business-unit/key=atlas-apac/approval-rules HTTP/1.1 Content-Type: application/json { "name": "APAC orders over 50,000 AUD", "status": "Active", "predicate": "totalPrice > \"50000.00 AUD\"", "requesters": [ { "associateRole": { "typeId": "associate-role", "key": "apac-buyer" } } ], "approvers": { "tiers": [ { "and": [ { "or": [ { "associateRole": { "typeId": "associate-role", "key": "apac-manager" } } ] } ] } ] } } ``` Creating or changing rules requires the `manage_approval_rules` scope; reading them requires `view_approval_rules`. In practice, only a high-authority role such as Business Administrator should hold `manage_approval_rules`. ## Writing the predicate: the currency-precision trap The `predicate` is an Order Predicate, so it can reference any field on the Order. The most common field is the order total, and it is also where the most common mistake lives. `totalPrice.centAmount` is a raw integer in the order's currency. Comparing it on its own ignores currency entirely: ```text totalPrice.centAmount > 5000000 ``` This matches 5,000,000 cents whether the Order is in Australian dollars, New Zealand dollars, or British pounds, which is almost never what you want for a multi-currency buyer like Atlas. There are two correct ways to make the comparison currency-safe. Pair the amount with the currency explicitly: ```text totalPrice.centAmount > 5000000 and totalPrice.currencyCode = "AUD" ``` Or use the money form, which carries the currency in the literal: ```text totalPrice > "50000.00 AUD" ``` Always constrain currency in a threshold predicate for a multi-currency Business Unit. A bare `centAmount` comparison will misfire across currencies, either demanding approval where it should not or, worse, letting a large order through because the numeric threshold did not match the currency you assumed. ## Predicates beyond the order total Order Predicates can inspect Order totals, shipping information, and Line Items. For the full list of supported Order fields and functions, see [Order Predicates](/api/projects/predicates.md#order-predicates). Use `lineItemExists` when the presence of a matching Line Item is enough, and use the Line Item `quantity` field inside the predicate when the rule depends on units within a matching Line Item. ```text lineItemExists(sku = "ZET-HAZMAT-01") = true ``` ```text lineItemExists(custom.category = "controlled" and quantity > 10) = true ``` Use `lineItemCount` only when the policy depends on the number of separate matching Line Items, not the units on one Line Item. These patterns mirror real customer rules: approval for orders containing specific products, tiered total thresholds, quantity-sensitive controlled products, and approval when shipping cost exceeds a limit. ## Predicates on custom business context When a policy depends on business context that has no native Order field, model that context as a Custom Field on the Cart or Order, then reference it in the predicate through the `custom.` path. For example, have your middleware tag an Order with a `custom.procurementCategory` field, and gate approval on it: ```text custom.procurementCategory = "capital-expenditure" ``` This is the mechanism for requirements that out-of-the-box fields do not express, such as internal budget categories, contract references, or a risk classification your systems already compute. Keep the Custom Field definition and the predicate in step: a predicate that references a field the Order does not carry never matches. ## Choosing requesters The `requesters` array lists the Associate Roles a rule applies to. It holds at least one [RuleRequester](/urn?urn=ctp%3Aapi%3Atype%3ARuleRequester), each pointing at an Associate Role by key. An Order needs approval under this rule only if the Associate who placed it holds one of these roles. [Separation of duties](/learning-model-b2b-commerce/configure-associate-access/associate-roles-and-permissions.md) becomes concrete in `requesters`. If `apac-buyer` is a requester and `apac-manager` is an approver, a buyer cannot self-approve, because the rule only applies to buyers and only managers can approve it. Design the two role sets so they do not overlap for any spend you want a second pair of eyes on. ## Worked example: Atlas APAC spending thresholds Atlas Corporate's APAC division, which trades in Australian dollars, wants two thresholds and one content-based rule. Model each as a separate Approval Rule on the `atlas-apac` Business Unit. 1. **Single-approver threshold.** Orders over AUD 2,000 placed by an `apac-buyer` need a manager. Predicate: `totalPrice > "2000.00 AUD"`; requester `apac-buyer`; a one-tier hierarchy requiring `apac-manager`. 2. **Dual-approver threshold.** Orders over AUD 5,000 additionally need the finance director. This uses the same requester and a higher threshold, but a different approver hierarchy. The next page covers that hierarchy. Predicate: `totalPrice > "5000.00 AUD"`. 3. **Controlled-product rule.** Any order containing a controlled item needs compliance sign-off regardless of value. Predicate: `lineItemExists(sku = "ZET-HAZMAT-01") = true`; requester `apac-buyer`; approver `compliance-reviewer`. Each rule sets `status: "Active"`, scopes its currency in the predicate, and names the requester role explicitly, so a multi-currency buyer's orders are governed precisely. ## Key takeaways - An Approval Rule belongs to one Business Unit and combines a `predicate`, `requesters`, an approver hierarchy, and a `status`; only `Active` rules are evaluated. - Constrain currency in any threshold predicate: pair `totalPrice.centAmount` with `totalPrice.currencyCode`, or use the money form such as `totalPrice > "50000.00 AUD"`. - Use `lineItemExists` (Boolean) and `lineItemCount` (Number) to trigger approval on order contents, and `shippingInfo.price.centAmount` to gate on shipping cost. - For business context with no native field, set a Custom Field on the Cart or Order and reference it in the predicate as `custom.`. - `requesters` are Associate Roles; keep requester and approver roles non-overlapping to enforce separation of duties. ## Related pages - [Area overview page with navigation](/learning-model-b2b-commerce.md) - [Previous page: Why procurement governance matters](/learning-model-b2b-commerce/configure-approval-workflows/why-procurement-governance-matters.md) - [Next page: Approver hierarchies and tiers](/learning-model-b2b-commerce/configure-approval-workflows/approver-hierarchies-and-tiers.md)