Rules-Based Promotion Expressions
Published by Todd Menier on September 20, 2018
Last updated on March 21, 2023
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)
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)
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"
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 contains a given value in an array:EligibleExpression:
"items.any(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:
"items.any(Product.xp.Tags.count(item = 'tag') = 3"ValueExpression:
"item.LineSubtotal * .15"
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 enabled
That'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.
Other Resources
Access to promotions is controlled via assignments If you do not want a user to be able to use a promotion, you would not assign it to them. Shoppers can list all promotions available to them via GET v1/me/promotions
.
Still have questions?
Ask in our Community Channel