About Promotions v2
On this page
Promotions v2 separates the question of when an offer applies from what it gives. That split is what makes offers possible that standard promotions cannot express: spend thresholds, tiers, free products, happy-hour windows, and promotions that need no code at all.
StoreConnect version 21 introduced this engine, alongside the original one described in Promotions. To set one up, see Configure Promotions v2. For ready-made recipes, see Promotions v2 examples. For the arithmetic behind a combined discount, see How Promotion v2 discounts are calculated. For in-store sales, see How Promotions v2 run at the register.
:::note
Promotions v2 runs on the web storefront when the store variable promotions.version is set to v2. Without it the web storefront runs the original engine. Registers always run Promotions v2, whatever this variable says. See Store variables.
:::
How a Promotion v2 is assembled
Four records work together, and a fifth is optional.
| Record | Purpose |
|---|---|
| Condition | What must be true, such as a cart reaching $50 |
| Reward | What the customer gets, such as 10% off |
| Promotion v2 | The header holding scheduling, scoping, stacking, and the coupon code |
| Promotion v2 Condition | Joins one condition to one reward under a promotion, with a priority |
| Promotion v2 Product Scope | Optional. Limits a condition or reward to given products, categories, brands, or shipping zones |
Conditions and rewards are reusable, so the same “cart reaches $100” condition can drive several promotions. The join record is what actually makes a promotion fire, and a promotion with no join record does nothing. A promotion whose conditions are never met gives nothing, even when everything else about it is valid.
Compared with standard promotions
| Capability | Standard promotions | Promotions v2 |
|---|---|---|
| Coupon-code promotions | Yes | Yes |
| Automatic promotions, no code | No | Yes |
| Cart conditions, such as spend or item count | No | Yes |
| Scheduling | Date range only | Dates, times, daily windows, days of week, weekly recurrence |
| Multiple reward tiers | No | Yes, tiered or stacked |
| Stacking between promotions | Always combines | Configurable per promotion |
| Channel scoping | No | Web or POS, and one store or outlet |
| Customer scoping | Contact, account, or membership, with multiple combinations per promotion | Contact, account, or membership, one value per promotion |
| Usage limits | Total only | Total and per customer |
| Codes per cart | One | Set by promotions.max_coupon_codes_per_cart |
| Usage tracking | Promotion Credits | Promotion 2 Usage, Reward Usage, and Order Item Reward |
Conditions
A condition decides whether its reward applies. Set the threshold in the condition’s Amount field where one is needed.
| Type | Applies when |
|---|---|
always_applies |
Always. Use it when the only restriction is on the promotion itself |
total_value |
The value of eligible cart items, taken at their sale price and before any Promotions v2 discount, reaches Amount |
total_quantity |
The quantity of eligible items reaches Amount |
product_count |
The number of distinct eligible products reaches Amount |
liquid_condition |
A Liquid expression evaluates as true |
Attach Promotion v2 Product Scope records to a condition to filter the cart before the condition is measured. With scopes attached, only matching items count toward the amount or quantity. A product scope also matches that product’s variants, and a brand scope also matches a variant whose master product carries the brand. On the web, a category scope matches products in that category and in the subcategories. At a register, it matches only products placed in the category itself.
The Currency field limits a condition to carts in one currency. Leave it blank to apply to all of them.
:::warning
The zone restriction itself works from any scope that names a Zone, whatever the Scope Type. But a zone scope whose Scope Type is Include is also read as a product include, and it matches no cart line, so it filters every item out. A total_value, total_quantity, or product_count condition carrying one can never reach the Amount.
Put the zone scope on an always_applies condition, which is measured before any item filtering, or give it a condition of its own. Leaving the Scope Type unset keeps the zone restriction and avoids the product filter.
An always_applies condition matches no items, so pair it with a reward whose Apply To is Specified Products, or with a subtotal reward. A product reward left on the default Matched Products discounts nothing, and the promotion does not apply.
:::
Liquid conditions
A liquid_condition reads Liquid Condition (Web) on the storefront. At a register a Liquid condition never fires, and Liquid Condition (POS) can only stop a promotion, when the register can reach the server and the template is false. See how Promotions v2 run at the register.
On the storefront, each non-blank line is evaluated as its own test and every line must be true. A blank template passes, and an expression that errors fails the condition.
Three variables are available: condition, cart, and cart_items, where cart_items holds the items left after product scoping.
This qualifies only carts shipping to Australia that hold at least three eligible items:
```liquid
cart.shipping_country_code == “AU” cart_items.size >= 3 ```
:::warning
These variables are the underlying Salesforce records, not the storefront Liquid objects of the same name, so the attribute names are different and the Cart Liquid object reference does not apply here. Writing cart.sub_total or cart.shipping_address.state, which are storefront attributes, silently fails the condition rather than reporting an error.
:::
Attributes confirmed to work on cart are shipping_street, shipping_city, shipping_state, shipping_state_code, shipping_postal_code, shipping_country, shipping_country_code, the matching billing_ fields, currencyisocode, and total. On each entry in cart_items you can use quantity, unit_price, and total_price. Use cart_items.size for a count.
Reach for a Liquid condition only when a built-in condition type cannot express the rule. A spend threshold is better set with total_value and an item count with total_quantity, both of which respect product scoping without any code.
:::note
Each line is wrapped in an {% if %} test, so write a comparison rather than a tag or an output statement.
:::
Rewards
| Type | What it does |
|---|---|
discount_on_subtotal |
Takes a percentage or fixed amount off the product subtotal, spread across the lines |
discount_on_products |
Takes a percentage or fixed amount off each matched or specified line |
discounted_shipping |
Reduces the delivery charge. Set Discount Percentage to 100 for free shipping |
free_product |
Adds free items, chosen by product scopes with scope type free_product |
fixed_price_for_n_products |
Sets Fixed Amount as the price for a group of Quantity items, or for every group when Frequency is no_limit |
:::warning
On discount_on_products with Discount Type Fixed, the Discount Amount comes off each matching line once, not off each unit. A $5 discount amount on a line of four items takes off $5, not $20. Use a percentage where the discount should scale with quantity.
:::
Product-level rewards take four more settings: Apply to chooses between the items the condition matched and the reward’s own scoped items; Max Eligible caps how many lines are discounted; when Max Eligible is set, Apply first to picks the most or least expensive lines first, by unit price; and Exclude Discounted and Exclude Free skip lines that already carry a discount or already cost nothing.
Only a discounted_shipping reward touches the delivery line. The delivery charge never counts toward a spend threshold, and subtotal and product rewards never reduce it.
:::note A line priced by a Block Price takes no Promotion v2 discount, and its value is left out of the subtotal that sizes a subtotal reward and caps a promotion. It still counts toward a promotion’s conditions. :::
Frequency decides how often a fixed reward repeats. once applies it a single time. no_limit repeats it for each time the condition was met, which is how “$5 off for every $50 spent” is built. A free product’s quantity and the number of fixed-price groups scale the same way.
:::warning
A percentage never repeats. no_limit on a percentage reward gives the same percentage however many times the condition was met, so 10% off stays 10% off. Where the discount is meant to multiply, use a discount_on_subtotal reward with Discount Type Fixed. On a discount_on_products reward a fixed discount comes off every matched line, and no_limit multiplies it per line.
:::
Reward strategy within a promotion
Reward Strategy on the promotion header controls what happens when several of its conditions match at once.
| Strategy | Behavior |
|---|---|
| Tiered | Only the matching condition with the lowest Priority number applies. This is the default and is what spend-and-save tiers need |
| Stacked | Every matching condition applies its reward |
Under Tiered, a link record with a blank Priority ranks last on the web, so it applies only when no other condition matches. At a register it ranks first instead (see how Promotions v2 run at the register).
Stacking between promotions
These settings decide whether a coupon can join what is already on the cart. Automatic promotions combine with each other whatever their stacking settings, unless one with Stop After This Promo applies first.
| Field | Value | Behavior |
|---|---|---|
| Stacking Behaviour | stackable |
Combines with other promotions. This is the default |
| Stacking Behaviour | not_stackable |
Conflicts with other promotions, resolved by Stacking Priority |
| Stacking Priority | existing_promotions |
The incoming coupon is refused whenever anything is already discounting the cart |
| Stacking Priority | biggest_reward |
The incoming coupon applies only if it gives more than the coupons already on the cart, and those coupons are then removed |
| Stop After This Promo | Checked | Automatic promotions evaluated after this one do not apply |
A promotion counts as “on the cart” only when it is actually giving a discount, not when it merely qualifies. The comparison adds the promotion’s currency discount to any points discount, and a free product contributes nothing to either, so an offer that only gives a gift always loses the comparison.
An automatic promotion running against a not-stackable coupon applies only if it gives more than that coupon, and a tie goes to the coupon.
Stop After This Promo is read on automatic promotions only, and a promotion that does not apply itself stops nothing. Because promotions without a subtotal reward are evaluated first, a flagged products promotion at Priority 10 still stops a subtotal promotion at Priority 1.
:::note
Two automatic promotions both marked not_stackable still both apply. The setting governs coupons.
:::
Evaluation order
Promotions are ranked by Priority, lowest number first, blank last. Two rules override that ordering:
- A promotion holding a
discount_on_subtotalreward is worked out after every promotion without one, so it sees their discounts. - Within a single promotion, product rewards are worked out before subtotal and shipping rewards.
On the web the order is: coupons without a subtotal reward, then the automatic promotions, then coupons with a subtotal reward. The web does not rank its coupons by Priority.
Applying or removing a coupon re-prices the automatic promotions in the same step. Other coupons already on the cart are re-priced the next time the web refreshes the cart.
Scheduling
| Field | Description |
|---|---|
| Start Date | First date the promotion is active. A promotion with no start date never applies |
| End Date | Last active date. Leave blank to run indefinitely |
| Start Time, End Time | Time of day on the start and end dates only, not a daily window |
| From Time, To Time | The daily window, applied every active day |
| Days of Week | Day numbers where 0 is Sunday and 6 is Saturday |
| Week Frequency | Recurrence in weeks from the start date, so 2 means every second week |
| Timezone | Timezone for every field above. Defaults to the store’s timezone |
An automatic promotion that has passed the End Date is removed from a cart the next time that cart is repriced, even if it was applied while the promotion was still running.
Scoping
Channels limits a promotion to the web store, the registers, or both. Select Web, POS, or both, or leave it blank to run the promotion everywhere, which is how every existing promotion behaves. A cart at a register counts as POS, and every other cart, including one built through the API, counts as Web.
:::note An upgraded org gets the Channels field but not the change to the Promotion v2 page layout, because a package upgrade does not change existing layouts. Add Channels to your layout to set it. See Managing StoreConnect upgrades. :::
Channel scope restricts a promotion to one Channel Scope Store or one Channel Scope Outlet. The two are mutually exclusive, and leaving both blank allows the promotion in every store and outlet.
Channel Scope Outlet matches whichever outlet the order belongs to. That is the outlet a shopper has selected on the web, for collection or local delivery, and the register’s own outlet at the point of sale. It is not a way to limit a promotion to the point of sale, because a web shopper can select an outlet too. Use Channels for that.
Customer scope restricts a promotion to one Customer Scope Contact, one Customer Scope Account, or one Customer Scope Membership. A membership is read from the customer’s account. A shopper who adds items before signing in gets an account- or membership-scoped promotion as soon as they sign in. At a register, only Customer Scope Contact can match, so a promotion scoped to an account or a membership never applies there.
Coupon codes and usage limits
Leave Coupon Code blank for a promotion that applies automatically. Populate it to require the customer to type the code at checkout, or a cashier to enter it at a register. Code matching ignores case and surrounding spaces.
A code scoped to a channel or a customer the shopper does not match reads back as invalid rather than as refused, so a customer cannot tell that a code they were not offered exists. A code outside its schedule reads as expired, whether it has ended or has not started yet.
:::warning The web’s coupon lookup is not limited to one store. Where two stores in the same org use the same Coupon Code, the lookup can find the other store’s promotion and report the code as invalid. Keep codes unique across stores. :::
Total Usage Limit caps uses across all customers and Per Customer Usage Limit caps them per customer. Leave either blank for no limit. Both are counted from the Promotion 2 Usage records written when an order is placed, so building a cart never uses up an allowance.
Per Customer Usage Limit only applies if the customer is known. A logged-in customer’s uses are counted. But when a shopper who is not logged in enters a code, there is no customer to match, so the limit counts every recorded use of the promotion by anyone. Once that total reaches the limit, every guest is refused with “You have already used this promo code the maximum number of times”. For a code that guests must be able to use, leave Per Customer Usage Limit blank.
What the customer sees
Automatic promotions apply as soon as the cart qualifies, with no customer action, and appear in the cart totals. They cannot be removed by the customer, and they withdraw on their own when the cart stops qualifying.
Coupon-code promotions appear under Applied codes in the checkout Promo Codes panel, and the customer can remove them. Automatic promotions are listed separately under Auto-Applied Discounts.
A free product reward adds its item to the cart at no charge, as its own line. If the cart stops qualifying, the free line is removed.
On the web, a coupon that qualifies but works out to no discount is refused at entry, and one already on the cart is removed the next time the cart is re-evaluated.
The cart is re-evaluated on nearly every page load, at each checkout step, and always before payment. A delivery or address change re-evaluates automatic promotions only, so a coupon that stops qualifying after one stays on the cart until the next full refresh.
:::note
The store variable promotions.max_coupon_codes_per_cart caps how many codes one cart accepts. When it is not set, there is no limit.
:::
How the discount is recorded
A promotion’s discount sits on the lines it discounted rather than on a separate line, and each line keeps a record of every reward that touched it, with the coupon code where there was one. The line’s stored unit price is already net of its promotions, and tax is charged on that reduced price.
A $20 product taxed at 20% with $8 off is charged as $12 plus $2.40 tax, a total of $14.40.
Placing the order writes a Promotion 2 Usage record for each promotion, a Reward Usage for each reward, and an Order Item Reward for each line a reward discounted. A free product’s line is recorded at $0, against the product’s list price.
Promotions v2 at the point of sale
The same promotions run at a register, with no enablement step. The register prices the cart itself from the promotion records synced to it, and asks the server only for what those records cannot answer, such as usage limits and the full schedule. A register runs only Promotions v2 and does not read promotions.version.
A few rules differ at a register: an account or membership customer scope never matches there, a Liquid condition never earns a reward and Liquid Condition (POS) can only stop one, and a points reward is not priced. For the full list, offline behavior, and what staff see, see How Promotions v2 run at the register. To enter a customer’s code at the counter, see Apply a coupon at the register.
How Promotions v2 and standard promotions interact
A store runs one engine on the web store, chosen by promotions.version. A coupon code is looked up only in that engine, so a code for the other engine reads as invalid, and a cart never takes a standard promotion and a Promotion v2 together.
A register always runs Promotions v2. On a store whose web store runs the original engine, the web store applies standard promotions while the registers apply any Promotion v2 records that are in date.
Products set to Exclude From Price Reductions with the Promotions value are left out by both engines, at the register and on the web. An excluded line takes no Promotion v2 reward, counts toward no condition, and adds nothing to the subtotal a subtotal reward is sized on. A Free Product reward that names the product still adds it free. See Exclude products from price reductions.
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.