{"title":"About Promotions v2","slug":"advanced-promotions","url":"https://support.storeconnect.com/articles/advanced-promotions","url_markdown":"https://support.storeconnect.com/articles/advanced-promotions.md","subtitle":null,"summary":"How the Promotions v2 engine works: reusable conditions and rewards, tiered and stacked strategies, stacking between promotions, scheduling windows, channel and customer scoping, coupon codes, and usage limits.","type":"Help_Documentation","video_url":"","keywords":"promotions v2, promotion v2, promotion2, advanced promotions, coupon code, automatic promotion, discount, stacking, scheduling, conditions, rewards, channel scope, customer scope, tiered, spend and save, free product, fixed price for n products, reward strategy, usage limits, liquid condition","last_modified":"2026-09-15T02:32:04+0000","body_markdown":"Promotions v2 separates the question of when an offer applies from what it gives. That split is what makes offers possible that [standard promotions](promotions) cannot express: spend thresholds, tiers, free products, happy-hour windows, and promotions that need no code at all.\n\nStoreConnect version 21 introduced this engine, alongside the original one described in [Promotions](promotions). To set one up, see [Configure Promotions v2](configure-advanced-promotions). For ready-made recipes, see [Promotions v2 examples](advanced-promotion-examples).\n\n## How a Promotion v2 is assembled\n\nFour records work together, and a fifth is optional.\n\n| Record | Purpose |\n|---|---|\n| **Condition** | What must be true, such as a cart reaching $50 |\n| **Reward** | What the customer gets, such as 10% off |\n| **Promotion v2** | The header holding scheduling, scoping, stacking, and the coupon code |\n| **Promotion v2 Condition** | Joins one condition to one reward under a promotion, with a priority |\n| **Promotion v2 Product Scope** | Optional. Limits a condition or reward to given products, categories, brands, or shipping zones |\n\nConditions 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.\n\n## Compared with standard promotions\n\n| Capability | Standard promotions | Promotions v2 |\n|---|---|---|\n| Coupon-code promotions | Yes | Yes |\n| Automatic promotions, no code | No | Yes |\n| Cart conditions, such as spend or item count | No | Yes |\n| Scheduling | Date range only | Dates, times, daily windows, days of week, weekly recurrence |\n| Multiple reward tiers | No | Yes, tiered or stacked |\n| Stacking between promotions | Always combines | Configurable per promotion |\n| Channel scoping | No | Store or outlet |\n| Customer scoping | Contact, account, or membership, with multiple combinations per promotion | Contact, account, or membership, one value per promotion |\n| Usage limits | Total only | Total and per customer |\n| Codes per cart | One | Set by `promotions.max_coupon_codes_per_cart` |\n| Usage tracking | Promotion Credits | Promotion 2 Usage, Reward Usage, and Order Item Reward |\n\n## Conditions\n\nA condition decides whether its reward applies. Set the threshold in the condition's **Amount** field where one is needed.\n\n| Type | Applies when |\n|---|---|\n| `always_applies` | Always. Use it when the only restriction is on the promotion itself |\n| `total_value` | The value of eligible cart items reaches **Amount** |\n| `total_quantity` | The quantity of eligible items reaches **Amount** |\n| `product_count` | The number of distinct eligible products reaches **Amount** |\n| `liquid_condition` | A Liquid expression evaluates as true |\n\nAttach **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 shipping zone scope instead restricts the condition to carts delivering into that zone.\n\nThe **Currency** field limits a condition to carts in one currency. Leave it blank to apply to all of them.\n\n### Liquid conditions\n\nA `liquid_condition` reads **Liquid Condition (Web)** on the storefront and **Liquid Condition (POS)** at the register. 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.\n\nThree variables are available: `condition`, `cart`, and `cart_items`, where `cart_items` holds the items left after product scoping.\n\nThis qualifies only carts shipping to Australia that hold at least three eligible items:\n\n```liquid\n\ncart.shipping_country_code == \"AU\"\ncart_items.size \u003e= 3\n```\n\n:::warning\nThese 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](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.\n:::\n\nAttributes 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.\n\nReach 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.\n\n:::note\nEach line is wrapped in an `{% if %}` test, so write a comparison rather than a tag or an output statement.\n:::\n\n## Rewards\n\n| Type | What it does |\n|---|---|\n| `discount_on_subtotal` | Takes a percentage or fixed amount off the cart subtotal |\n| `discount_on_products` | Takes a percentage or fixed amount off matched or specified products |\n| `discounted_shipping` | Reduces shipping. Set **Discount Percentage** to `100` for free shipping |\n| `free_product` | Adds free items, chosen by product scopes with scope type `free_product` |\n| `fixed_price_for_n_products` | Sets **Fixed Amount** as the price for **Quantity** matching items |\n\nProduct-level rewards take four more settings: **Apply to** chooses between the items the condition matched and the reward's own scoped items; **Apply first to** discounts the most or least expensive first; **Max Eligible** caps how many items are discounted; and **Exclude Discounted** and **Exclude Free** skip items that already carry a discount or already cost nothing.\n\n**Frequency** decides how often a reward repeats. `once` applies it a single time. `no_limit` repeats it for each qualifying group, which is how \"10% off for every three items\" is built.\n\n## Reward strategy within a promotion\n\n**Reward Strategy** on the promotion header controls what happens when several of its conditions match at once.\n\n| Strategy | Behavior |\n|---|---|\n| Tiered | Only the matching condition with the lowest priority number applies. This is the default and is what spend-and-save tiers need |\n| Stacked | Every matching condition applies its reward |\n\n## Stacking between promotions\n\nSeveral promotions can be active at once, and these settings decide whether they combine.\n\n| Field | Value | Behavior |\n|---|---|---|\n| **Stacking Behaviour** | `stackable` | Combines with other promotions. This is the default |\n| **Stacking Behaviour** | `not_stackable` | Conflicts with any other promotion in the cart |\n| **Stacking Priority** | `existing_promotions` | On a conflict, keep what is already applied and reject the new one |\n| **Stacking Priority** | `biggest_reward` | On a conflict, keep whichever gives the larger discount |\n| **Stop After This Promo** | Checked | Stop evaluating further promotions once this one applies |\n\nPromotions are evaluated in ascending **Priority**, so lower numbers run first. Where a conflict involves a promotion set to `not_stackable` with `existing_promotions`, the incoming promotion is rejected outright.\n\nThe store variable `promotions.calculation_strategy` decides how combined discounts are calculated: `independent`, the default, works from the original item prices, while `cumulative` works from the running subtotal after earlier discounts. See [Store variables](store-variables).\n\n## Scheduling\n\n| Field | Description |\n|---|---|\n| **Start Date** | First date the promotion is active. A promotion with no start date never applies |\n| **End Date** | Last active date. Leave blank to run indefinitely |\n| **Start Time**, **End Time** | Time of day on the start and end dates only, not a daily window |\n| **From Time**, **To Time** | The daily window, applied every active day |\n| **Days of Week** | Day numbers where `0` is Sunday and `6` is Saturday |\n| **Week Frequency** | Recurrence in weeks from the start date, so `2` means every second week |\n| **Timezone** | Timezone for every time field. Defaults to the store's timezone |\n\n## Scoping\n\n**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 on every channel.\n\n**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.\n\n## Coupon codes and usage limits\n\nLeave **Coupon Code** blank for a promotion that applies automatically. Populate it to require the customer to type the code at checkout. Code matching is not case sensitive.\n\n**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.\n\n## What the customer sees\n\nAutomatic 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.\n\nCoupon-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**.\n\nA 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.\n\n:::note\nThe store variable `promotions.max_coupon_codes_per_cart` caps how many codes one cart accepts. When it is not set, there is no limit.\n:::\n\n## How Promotions v2 and standard promotions interact\n\nBoth engines evaluate independently, so a cart can receive discounts from a standard promotion and a Promotion v2 at the same time. To prevent overlap, scope your Promotions v2 records by channel or customer, or move your offers to one engine entirely.\n\n:::warning\nProducts excluded from promotions are skipped only by standard promotions. The **Promotions** value of **Exclude From Price Reductions** has no effect on Promotions v2, so use a **Promotion v2 Product Scope** with scope type `exclude` to keep a product out of one. See [Exclude products from price reductions](exclude-products-from-price-reductions).\n:::"}