Bulky products, such as bicycles, fitness equipment, furniture, and appliances, can carry a separate delivery charge. Offer free shipping to selected customers when the basket margin can cover the delivery cost.
This scenario uses churn and category purchase propensity to select an audience for a free shipping offer. Jinjava calculates basket eligibility at render time, using the latest cart.status and product attributes from the feed.
The example uses a sporting goods retailer selling bicycles and bulky fitness equipment. An optional Lookalikes prediction extends the audience to profiles similar to previous buyers. You can also run an A/B test to assess the offer's effect on conversion and profitability.
Prerequisites
- Implement website tracking and transaction tracking, including
product.buyandtransaction.charge. Installing the tracking code alone does not describe the transaction integration. See the prerequisites in Send propensity-based customers to Facebook. - Configure an item feed for Predictions. Include category data and make the category attribute filterable. Keep product identifiers consistent between the feed, purchase events, and basket events. See Creating propensity predictions.
- Enable Classification and Propensity. Enable Lookalikes if you want to extend the audience. Check data requirements before calculating models; enabling a model does not ensure that every profile has enough data for a score. See Introduction to Predictions and Enabling predictions.
- Send the complete
cart.statusafter every basket change and send an empty basket after checkout. See Tracking cart status. - Include price, margin, and the attributes that identify qualifying bulky products in the product feed. Define whether margin is a rate or an amount per unit. Use current quantities from
cart.statusand define delivery cost and customer delivery charge from your event data, feed, or delivery rules. The example uses fixed illustrative values. Use one currency and a consistent tax basis. - Create a voucher pool with codes recognized by the store. Implement usage restrictions and redemption in the store, including what happens when a code has expired or has already been used. See Voucher pools.
- Make the latest basket event available before rendering the offer. After a relevant basket change, update
cart.statusand render the offer again so Jinjava recalculates eligibility.
IMPORTANT: This scenario assumes that the events and product feed contain all data required for the calculation. Jinjava performs the calculation when the content is rendered. The Jinjava example below illustrates the calculation; adapt its fields, margin units, and delivery assumptions to your setup.
Process
In this use case, you will go through the following steps:
- Define the scenario settings.
- Create the churn target and prediction.
- Create a propensity prediction for bulky categories.
- Use the latest prediction results to build the campaign audience.
- Optionally, extend the audience with Lookalikes.
- Calculate basket eligibility in Jinjava.
- Display the free shipping code on the basket.
- Verify the scenario before activation.
Define the scenario settings
Record these values before building the campaign. The time ranges below are examples. Adjust them to your customers' purchase patterns.
| Setting | Example or decision required |
|---|---|
| Bulky categories | Bicycles and the retailer's selected bulky fitness categories |
| Churn definition | A previous buyer with no transaction in the last 60 days |
| Churn forecast horizon | Select explicitly to match the retention question; this is separate from the 60-day inactivity definition |
| Churn recalculation | Every 7 days |
| Propensity audience | At least 3 sessions and no bulky-category purchase in the last 3 months |
| Propensity and optional lookalike recalculation | Every 10 days |
| Maximum score age | Set for each model, allowing for calculation time; exclude results older than this limit |
| Contribution buffer | The minimum amount that must remain after delivery; include other variable costs in the rule if your business requires it |
Choose the churn definition from actual purchase patterns. A 60-day gap can be normal for durable goods. One historical transaction establishes that a person is a previous buyer; it does not establish loyalty.
Create the churn target and prediction
Create the segmentations
- Go to Decision Hub > Segmentations > New segmentation.
- Name it
Previous buyers. - Add a condition for a performed
transaction.chargeevent and set its time range to Lifetime. - Save the segmentation.
- Create another segmentation named
Previous buyers inactive for 60 days. - Require membership in
Previous buyers. - Add an AND condition: no
transaction.chargein the Last 60 days. - Save it.
The second segmentation defines the state to predict. It is not itself a prediction. This follows the segmentation-to-expression pattern from Predict churn, with a different inactivity definition.
Create the target expression
- Go to Behavioral Data Hub > Expressions > New expression.
- Name it
Churn signal target. - Select Attribute in Expressions for.
- Select Function > If.
- Use Previous buyers inactive for 60 days as the condition,
1as the true result, and0as the false result. - Save the expression.
Configure Classification
- Go to AI Hub > (AI Predictions) Models > New prediction.
- Select Create from scratch > Classification and name the model
Churn risk - previous buyers. - Select Previous buyers as the audience and Churn signal target as the target expression.
- Review the input events. Keep automatic selection unless the data requires a deliberate change.
- In Model configuration, set the recalculation interval and explicitly select the forecast horizon.
- Enable Set up recurring prediction calculation and select the 5-point scale.
- If configuring the event's score name, use
previous_buyers_churn. - Apply the settings and select Save & Calculate.
Results use snr.prediction.score. Record the model ID for later filtering. Follow Creating classification and regression predictions for the full configuration.
Create a propensity prediction for bulky categories
The audience below captures repeat site visitors who have not recently bought from the selected categories. It does not establish that they are hesitant or that delivery is their barrier.
Prepare the audience
- Go to Behavioral Data Hub > Live Aggregates > Create aggregate.
- Select Profile.
- Name the aggregate
Sessions in the last 3 months. - Select Count.
- Select the
session.startevent, which is generated when a visitor starts a session. - Set the period to Last 3 months.
- Save the aggregate.
- Create a segmentation named
Repeat visitors without a recent bulky purchase. - Require the session aggregate to be greater than or equal to
3. - Add an AND condition: no
product.buymatching the selected bulky categories in the Last 3 months. Match the category filter to the actual event values. Use the same category set in the prediction and basket eligibility rules. - Save the segmentation.
Configure Propensity
- Create a prediction using Create from scratch > Propensity.
- Name it
Propensity to buy bulky itemsand select the segmentationRepeat visitors without a recent bulky purchaseas its audience. - In Item feed, select the configured feed and define an item filter for the bulky categories.
- Review the matching items before saving the filter.
- Enable recurring calculation, using the chosen interval, and keep the 5-point scale.
- Select Save & Calculate and record the model ID.
The output event is snr.propensity.score. A low result means a low predicted propensity to buy the selected products. See Creating propensity predictions and the comparable attribute-based propensity use case.
Use the latest scores to build the campaign audience
Use the most recent result for each model. A condition that matches any historical high or low score can retain a customer after their score changes.
Create score aggregates
Create one aggregate for each prediction:
| Aggregate name | Event | Parameter | Event filter |
|---|---|---|---|
Latest churn label |
snr.prediction.score |
score_label |
modelId equals the churn model ID |
Latest bulky propensity label |
snr.propensity.score |
score_label |
modelId equals the propensity model ID |
For each aggregate:
- Go to Behavioral Data Hub > Live Aggregates > Create aggregate.
- Select Profile and the Last analysis.
- Select the event, parameter, and event filter from the table.
- Limit the relative period to the maximum score age defined for that model.
- Save the aggregate.
Filter the aggregate by modelId, not by the desired label. Filtering for high before taking Last would return the latest high result, even if a newer low result exists.
The SMS cost use case shows the latest-score aggregate pattern. Use the event names above, which follow the model documentation.
Create the audience segmentation
- Go to Decision Hub > Segmentations > New segmentation.
- Name it
Free shipping - risk audience. - Add a condition:
Latest churn labelishighorvery high. - Add an OR condition:
Latest bulky propensity labelisvery low,low, ormedium. - Save the segmentation.
Treat missing or expired results as no match for that condition. The other condition can still qualify the profile. Check the exact event values during verification; the documented five-point labels are very low, low, medium, high, and very high. See Predictions glossary.
These scores select customers for the offer.
Optionally, extend the audience with Lookalikes
Use Lookalikes to reach customers similar to previous bulky-item buyers. This is an audience extension, not another measure of churn risk.
- Create a segmentation named
Previous bulky-item buyerswith aproduct.buycondition matching the selected categories. Set an explicit historical period suitable for your catalog and buying cycle. - Create a segmentation named
Recognized visitors active in the last 90 days. ExcludePrevious bulky-item buyersand the currentFree shipping - risk audience. - Create a Lookalikes prediction. Select
Previous bulky-item buyersas the source andRecognized visitors active in the last 90 daysas the target. - Enable recurring calculation at the chosen interval and use the 5-point scale.
- Calculate the prediction and record its model ID.
- Create an aggregate named
Latest bulky lookalike labelusing Last, thescore_labelparameter ofsnr.lookalike.score, a filter for that model ID, and the chosen maximum score age. - Create a segmentation named
Free shipping - lookalike audienceforhighorvery highresults. Reapply the exclusions ofPrevious bulky-item buyersandFree shipping - risk audiencewhen selecting the campaign audience.
To include these profiles in the offer, create a segmentation named Free shipping - campaign audience with membership in Free shipping - risk audience OR Free shipping - lookalike audience. Use this combined audience in Dynamic content. Otherwise, use Free shipping - risk audience on its own.
Lookalike similarity supports reach extension. It does not supply a missing churn or purchase-risk signal. See Creating lookalikes predictions.
Calculate basket eligibility in Jinjava
Jinjava calculates eligibility at render time. A Last aggregate reads the product IDs from the latest cart.status. For each ID, the template retrieves the corresponding product from the feed and combines its attributes with the basket quantities. The example uses fixed delivery values; replace them with the appropriate values or rules for your setup.
Prepare the latest basket data
- Go to Behavioral Data Hub > Live Aggregates > Create aggregate.
- Select Profile, name it
Latest basket products, and select Last. - Select
cart.statusand itsproductsparameter. In this example, each object contains the product ID inskuand itsquantity. - Set the period to Lifetime and save the aggregate.
- Send an empty basket after checkout so this aggregate does not retain the completed order.
- Use the product IDs and quantities from the same
cart.statusevent. If your existing aggregate returns onlyitemIds, obtain the matching quantities from the same event and adapt the loop below. - In the Jinjava template, read the aggregate, iterate over the IDs, and use
catalogitemv2to retrieve each product's price, margin, and bulky-item attributes from the feed-backed catalog. See Insert usage.
Take all inputs from the same, latest cart.status event. A feed price used in this calculation must represent the applicable selling price. If an event carries a basket-specific discount, account for it before calculating margin.
Define the calculation
Use amounts in one currency and on the same tax basis. The example uses Synerise inserts, including the catalog-object insert catalogitemv2, the documented do … append syntax, and the sum filter. It collects line margins in a list and sums them after the loop. See Jinjava tags, Jinjava filters, and Insert usage.
Check the Jinjava example
{# Illustrative values: adapt delivery rules and margin units to your store. #} {% set delivery_cost = 80 %} {% set customer_shipping_charge = 80 %} {% set required_contribution_buffer = 20 %} {% aggregate LAST-BASKET-PRODUCTS-AGGREGATE-ID %} {% set margins = [] %} {% set bulky_items = [] %} {% set invalid_items = [] %} {% for item in aggregate_result %} {% if item.sku is defined and item.quantity is defined %} {% set sku = item.sku %} {% catalogitemv2.CATALOG-NAME(sku) allowEmpty=True %} {% if catalog_result.price is defined and catalog_result.price != null and catalog_result.margin is defined and catalog_result.margin != null and catalog_result.bulky is defined and catalog_result.bulky != null %} {% set price = catalog_result.price|float(-1) %} {% set margin_rate = catalog_result.margin|float(-1) %} {% set quantity = item.quantity|float(-1) %} {% if price > 0 and quantity > 0 and margin_rate >= 0 and margin_rate <= 1 and (catalog_result.bulky == true or catalog_result.bulky == false) %} {% do margins.append(price * margin_rate * quantity) %} {% if catalog_result.bulky == true %} {% do bulky_items.append(sku) %} {% endif %} {% else %} {% do invalid_items.append(sku) %} {% endif %} {% else %} {% do invalid_items.append(sku) %} {% endif %} {% endcatalogitemv2 %} {% else %} {% do invalid_items.append('missing basket fields') %} {% endif %} {% endfor %} {% set basket_margin = margins|sum %} {% if invalid_items|length == 0 and bulky_items|length > 0 and customer_shipping_charge > 0 and basket_margin - delivery_cost >= required_contribution_buffer %} <p>Your free shipping code: {% voucher assign=false %} FREE-SHIPPING-POOL-UUID {% endvoucher %} </p> {% endif %} {% endaggregate %}This example assumes a boolean bulky feed attribute, a numeric price, and a margin rate stored as a fraction. Replace the aggregate ID, catalog name, voucher pool ID, and attribute names. The products aggregate returns objects containing sku and quantity; the SKU is the key used to read the product feed.
The values 80, 80, and 20 are illustrative delivery cost, customer delivery charge, and remaining-margin buffer. Replace them with your rules or values from the events and feed. If margin is stored as a percentage, divide it by 100. If it is an amount per unit, multiply it by quantity without multiplying by price again. Adapt discount handling and included costs to your pricing model.
Supply validated numeric fields in the feed and event. The example expects a positive price and quantity, a margin rate between 0 and 1, and a boolean bulky. A missing property produces no output by itself. Here, is defined checks whether the calculation has the required data; the example withholds the offer when data is missing or its checks fail. The allowEmpty=True option lets the template inspect a missing catalog item and withhold the offer. Adjust these example rules if your catalog includes free items or negative margins.
Check the template in Dynamic content Preview with your own aggregate and catalog. The example illustrates the calculation; it does not define your delivery policy.
For example, a basket worth 200 currency units with a 20% product margin contributes 40 before delivery. If delivery costs 80, the remaining contribution is -40. A 20% margin threshold alone would not reject this basket. All values in this example are illustrative.
Document the basis of the feed margin. If the feed price and margin already describe the applicable selling price, use them directly. If a basket discount changes that price, adjust the calculation using the event data; do not reuse the original percentage without checking its basis.
Apply the rule throughout the basket flow
- Send the complete
cart.statusafter prices, quantities, or the delivery option change. - Render the content after the latest basket data is available to the aggregates.
- In Jinjava, retrieve the products from the feed, calculate the contribution, and check the bulky-item and paid-delivery conditions.
- Hide the outdated offer after a basket change and render the content again. Jinjava then recalculates the condition from the updated inputs.
- Retrieve and display the voucher only inside the successful eligibility branch in the offer template.
- Apply the store's voucher usage restrictions at checkout and record redemption for reporting.
The voucher documentation assigns usage logic to the integrated application. A condition in a message template controls display; it does not enforce the discount at checkout.
Display the free shipping code on the basket
- Go to Experience Hub > Dynamic content > Create new and choose Insert Object.
- Name the campaign
Free shipping for bulky items. - In Audience, select
Free shipping - risk audience, orFree shipping - campaign audienceif you added Lookalikes. - In Content, select Simple message, set the CSS selector and insertion position near the order summary, and create the offer template.
- Include the Jinjava example in the template and replace its placeholders. Add your banner design around the content inside the successful eligibility branch.
- Target the basket page and set the campaign schedule. If using On landing, also implement the basket-change refresh described above.
- Save the campaign and verify the scenario before activation.
The calculation already includes the voucher insert. It retrieves a bound code for repeat display only when the basket meets the example conditions. Define the behavior for a redeemed or expired code according to your voucher policy. Use a separate pool in Preview because rendering voucher inserts can consume codes. See Insert usage and Creating dynamic content.
Verify the scenario
Use test profiles and the test voucher pool. Check both the displayed result and the store's response at checkout.
| Test | Expected result |
|---|---|
| Old high churn result followed by a current low result | Churn branch does not qualify the profile |
| No current score for either risk model | Risk audience does not qualify the profile |
| High lookalike result only | Profile qualifies only if the optional audience extension is enabled |
| Eligible profile, qualifying bulky item, paid delivery, sufficient contribution | Offer can display; the store validates the code |
| Eligible profile, basket with no qualifying bulky item | No offer |
| Shipping already free | No additional free shipping offer |
| Delivery cost exceeds available contribution | No offer |
| Missing cost or pricing data | No offer until a valid decision is available |
| Customer removes the bulky item after seeing the code | Updated render shows no offer; the store applies its configured code restrictions |
| Delivery option changes | Eligibility is recalculated |
| Code is redeemed or expired | Store applies the configured usage restrictions |
| Transaction completes | Empty cart.status is sent; order and redemption are recorded |
Confirm that the aggregate returns the expected basket objects and that feed fields have the specified types. After Preview and checkout checks pass, activate the campaign according to its schedule.
Measure the impact with an A/B test
This step is optional. Use an A/B test if you want to assess whether the free shipping offer improves conversion and profitability.
- In the campaign's Content section, choose A/B tests.
- Create two variants with a fixed allocation, for example 50/50:
- Control: no free shipping code. Keep the basket unchanged and do not include the voucher insert.
- Challenger: the offer template with the Jinjava calculation and voucher insert. Show the code only when the basket meets the conditions.
- Keep automatic allocation off during the comparison and use the same campaign audience and page targeting for both variants.
- Confirm in Preview that Control shows no code and Challenger follows the basket conditions. Verify how both variants are recorded in campaign events before measuring results.
- Define the reporting window and compare conversion rate, revenue per profile, and contribution after delivery costs between Control and Challenger.
Include all profiles assigned to each variant in the analysis, including those who do not purchase and those in Challenger whose baskets do not qualify. Comparing only voucher users against all Control profiles would compare different populations.
For profitability, use realized order contribution after delivery and other costs included in your accounting model. Include shipping revenue where charged, and handle returns and cancellations consistently. A revenue increase alone does not establish a profit increase.
Dynamic content variant allocation uses browser storage and lasts 30 days. Account for browser changes and reassignment when defining the test window. See Creating dynamic content.