Rules-Based Promotion Expressions
Published by Todd Menier on September 20, 2018
Last updated on June 26, 2024
Writing custom rule-base expressions for your order promotions may seem intimidating at first, but once you get the hang of it you'll realize there are virtually limitless possibilities which provide the flexibility to meet almost any business need. In fact, custom order promotions are a great example of how our core value Flexibility over Features informs everything we build (and don't build) into the platform. We've found that allowing developers to define their own custom if-then statements is incredibly robust and solves more problems than providing a limited set of non-customizable promotions out-of-the-box.
Promotions Overview
Every promotion requires two rule expressions, an EligibleExpression
, which evaluates the current state of the order and returns true or false indicating whether the promotion can be applied to the order, and a ValueExpression
, which evaluates the order and returns a monetary value, which is then subtracted from the order subtotal.
There are several properties that you can include in both expressions to build your custom promotion: order
supports the same properties as the Order model returned from v1/orders
API endpoints, including xp. items
supports the following functions:
items.any()
true if any item on the order matches filter
items.all()
true if all items match filter
items.quantity()
returns sum of line item quantities matching your specified condition
items.count()
returns number of line items on the order matching your specified condition
items.total()
compare result to a dollar amount
product.incategory()
true if a product is directly assigned or the specified category
product.inparentcategory()
true if a product is assigned to the specified category or any of it's children, at any depth
A single property supports in()
(allows rule engine to check if a property value exists in the comma delimited list.)
Array properties supports the following functions:
contains()
(allows rule engine expressions to check if a value exists in an array. Both string and number arrays are supported.)count()
(returns number of array items matching your specified condition)any()
(true if any array item matches filter)all()
(true if all array items match filter)
Here are a few examples:
order.xp.myarray.contains('value2')
(true if Order.xp.myarray contains the value "value2")order.xp.myarray.count() = 3
(true if Order.xp.myarray has 3 items)order.xp.myarray.any(item = 'four')
(true if Order.xp.myarray has item = 'four')order.xp.Tags.all(item = 'tag*') = true
(true if Order.xp.Tags all items start with 'tag')items.any(Product.xp.Tags.contains('value2')
(true if a lineitem contains a product where Product.xp.Tags contains the value "value2")item.Product.xp.NumberArray.contains(23)
(true if a lineitem contains a product where Product.xp.NumberArray contains the value 23)item.product.xp.myarray.any(item = 20)
(true if a lineitem has a product where Product.xp.myarray has any item = 20)item.product.xp.Tags.count(item = 'tag*') = 3
(true if a lineitem has a product where Product.xp.Tags has 3 items that start with 'tag')
The following operators are all supported in rule engine expressions:
Comparison:
=
,<
,>
,<=
,>=
Logical:
and
,or
andnot
Mathematical:
+
,-
,*
,/
and%
Additionally, you can wrap any of your expressions in these functions:
min(...)
returns the smaller of the two arguments
max(...)
returns the larger of the two arguments
ifs(...)
Returns a value based on evaluating a number of conditions, returning a default if none are true
The last parameter in the expression is used as the default
Limiting Promotion Eligibility
Can only be used on Promotions where LineItemLevel
is true.
ItemLimitPerOrder
Positive integer that defines the maximum number of items a given promotion will be applied to
ValueExpression
will be the discount applied to each eligible itemCannot be used in conjunction with
QuantityLimitPerOrder
QuantityLimitPerOrder
Positive integer that defines the maximum quantity of eligible line items
ValueExpression
will be the discount applied to each eligible quantityCannot be used in conjunction with
ItemLimitPerOrder
ItemSortBy
Comma delimited string of line item properties
Sort descending by using
!
Supports sorting on xp
If no value is provided, items will be sorted by the default value which is
DateAdded
ascending
Sample use case: 30% off the least expensive 3 items if a user spends at least $50
1{2 "Code": "30OFF",3 "LineItemLevel": true,4 "ItemLimitPerOrder": 3,5 "ItemSortBy": "LineSubtotal",6 "EligibleExpression": "order.Subtotal >= 50",7 "ValueExpression": "item.LineSubtotal * .3"8}
Examples
$10 off any order greater than $50:EligibleExpression
: "order.Subtotal > 50"ValueExpression
: "10"
Free Shipping when you spend $60:EligibleExpression
: "order.Subtotal >= 60"ValueExpression
: "order.ShippingCost"
BOGO (limited to 1 free item):EligibleExpression
: "items.quantity(ProductID = 'ABC') > 1"ValueExpression
: "items.total(ProductID = 'ABC') / items.quantity(ProductID = 'ABC')"
$5 off the order total when any line item has a given product ID:EligibleExpression
: "items.any(ProductID = '123')"ValueExpression
: "5"
5% off all line items with a product with ID in the given list:EligibleExpression
: "item.ProductID.in('ID1', 'ID2', 'ID3')" ValueExpression
: "item.LineSubtotal * .05"
15% off all line items with a product assigned to a given CategoryID:EligibleExpression
: "item.product.incategory('Bikes')"ValueExpression
: "item.LineSubtotal * .15"
10% off when all products are on sale with a maximum promotion discount of $20 (utilizing product xp):EligibleExpression
: "items.all(Product.xp.OnSale = true)"ValueExpression
: "min(order.Subtotal * .1, 20)"
30% off when you buy 10 or more products assigned to a given CategoryID:EligibleExpression
: "items.quantity(product.incategory('GuitarAccessories')) >= 10"ValueExpression
: "items.total(product.incategory('GuitarAccessories')) * .3"
20% off when you buy these 2 products together (no quantity limit):EligibleExpression
: "items.any(ProductID = 'ABC') and items.any(ProductID = 'XYZ')"ValueExpression
: "(items.total(ProductID = 'ABC') + items.total(ProductID = 'XYZ')) * .2"
10% off your entire order when you spend more than $200 in these categories:EligibleExpression
: "items.total(product.incategory('Kitchen')) + items.total(product.incategory('Bedding')) + items.total(product.incategory('Bathroom')) > 200"ValueExpression
: "order.Subtotal * .1"
$50 off line items from a given supplier when you spend more than $100 on that supplier's products:EligibleExpression
: "item.SupplierID = '123' and items.total(SupplierID = '123') >= 100"ValueExpression
: "50 / items.count(SupplierID = '123')"
25% off a user's first order (utilizing user xp):EligibleExpression
: "order.FromUser.xp.FirstOrder = true"ValueExpression
: "order.Subtotal * .25"
10% off orders for all registered users:
EligibleExpression
: "not (order.FromUser.ID = 'myDefaultAnonUserID') and order.Subtotal > 0"ValueExpression
: "order.Subtotal * .10"
15% off your entire order when order xp property in the array (utilizing user xp): EligibleExpression
: "order.xp.foo.in('bar','brr','brb')" ValueExpression
: "order.Subtotal * .15"
BOGO (scales with quantity):EligibleExpression
: "items.quantity(ProductID = 'XYZ') > 1"ValueExpression
: "((items.quantity(ProductID='XYZ')/2) - (items.quantity(ProductID='XYZ') % 2 * .5)) * items.total (ProductID='XYZ') / items.quantity(ProductID='XYZ')"
10% off all line items where the Product xp contains a given value:EligibleExpression:
"item.product.xp.Tags.contains('XYZ')"ValueExpression:
"item.LineSubtotal * .10"
15% off all line items where the Product contains a given number of items in an array that start with a given expression:EligibleExpression:
"item.product.xp.Tags.count(item = 'tag') = 3"ValueExpression:
"item.LineSubtotal * .15"
5%, 10% or 15% off discount on products assigned to a given category based on spend threshold between $10-$50:EligibleExpression:
"items.total(product.incategory('A') >= 10 and item.product.incategory('A')"ValueExpression:
"ifs(items.total(product.incategory('A')) >= 50, item.LineSubtotal .15, items.total(product.incategory('A')) >= 30, item.LineSubtotal * .10, item.LineSubtotal * .05)"
Expressions using Order and Item History
$5 off if a user has placed more than four orders in the past yearEligibleExpression
: orderhist.count('1Y') > 4ValueExpression
: 5
10% off if a user has ordered a given product 10 or more times in the past 6 monthsEligibleExpression
: itemhist.quantity('6M', 'Product.ID = {productID}') >= 10ValueExpression
: order.Subtotal * .10
1% of the total of all orders placed in the past monthEligibleExpression
: orderhist.count('1M') >= 1ValueExpression
: orderhist.total('1M') * .01
orderhist
and itemhist
can only be used in expressions if the marketplace has Premium Search for Orders enabledThat's a lot of flexibility. What else should I know?
Considerations
Both
EligibleExpression
andValueExpression
are limited to 400 characters each. Use them wisely. If you're bumping up against the character limit, there's a good chance you could optimize your promo expressions in some way.Promotion eligibility is evaluated by the api at the time the promotion is applied, and then again on order submit.
Enhancements Roadmap
v1/orders/{direction}/{orderID}/validate
endpoint that will check validity of an order in it's current state, including, but not limited to, applied promotions (released in v1.0.131)Support for
min(...)
andmax(...)
functions (released in v1.0.165)product.inparentcategory()
- the current functionproduct.incategory()
only recognizes direct assignments, the new function will look for products assigned to any child categories of the parent that is passed in. (released in v1.0.350)
Other Resources
GET v1/me/promotions
.Still have questions?
Ask in our Community Channel