# Offer free shipping on bulky items based on purchase risk and basket margin


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.buy` and `transaction.charge`. Installing the tracking code alone does not describe the transaction integration. See the prerequisites in [Send propensity-based customers to Facebook](/use-cases/send-propensity-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](/docs/ai-hub/predictions/propensity).
- 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](/docs/ai-hub/predictions/predictions-introduction) and [Enabling predictions](/docs/ai-hub/predictions/enabling-predictions).
- Send the complete `cart.status` after every basket change and send an empty basket after checkout. See [Tracking cart status](/developers/web/cart).
- 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.status` and 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](/docs/assets/code-pools).
- Make the latest basket event available before rendering the offer. After a relevant basket change, update `cart.status` and render the offer again so Jinjava recalculates eligibility.


<div class="admonition admonition-important"><div class="admonition-icon"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg></div><div class="admonition-body"><div class="admonition-content">

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.

</div></div></div>


## Process
---

In this use case, you will go through the following steps:
1. [Define the scenario settings](#define-the-scenario-settings).
2. [Create the churn target and prediction](#create-the-churn-target-and-prediction).
3. [Create a propensity prediction for bulky categories](#create-a-propensity-prediction-for-bulky-categories).
4. [Use the latest prediction results to build the campaign audience](#use-the-latest-scores-to-build-the-campaign-audience).
5. [Optionally, extend the audience with Lookalikes](#optionally-extend-the-audience-with-lookalikes).
6. [Calculate basket eligibility in Jinjava](#calculate-basket-eligibility-in-jinjava).
7. [Display the free shipping code on the basket](#display-the-free-shipping-code-on-the-basket).
8. [Verify the scenario before activation](#verify-the-scenario).

## 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
1. Go to **Decision Hub > Segmentations > New segmentation**.
2. Name it `Previous buyers`.
3. Add a condition for a performed `transaction.charge` event and set its time range to **Lifetime**.
4. Save the segmentation.
5. Create another segmentation named `Previous buyers inactive for 60 days`.
6. Require membership in `Previous buyers`.
7. Add an **AND** condition: no `transaction.charge` in the **Last 60 days**.
8. 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](/use-cases/churn-prediction), with a different inactivity definition.

### Create the target expression
1. Go to **Behavioral Data Hub > Expressions > New expression**.
2. Name it `Churn signal target`. 
3. Select **Attribute** in **Expressions for**.
4. Select **Function > If**.
5. Use **Previous buyers inactive for 60 days** as the condition, `1` as the true result, and `0` as the false result.
6. Save the expression.

### Configure Classification
1. Go to **AI Hub > (AI Predictions) Models > New prediction**.
2. Select **Create from scratch > Classification** and name the model `Churn risk - previous buyers`.
3. Select **Previous buyers** as the audience and **Churn signal target** as the target expression.
4. Review the input events. Keep automatic selection unless the data requires a deliberate change.
5. In **Model configuration**, set the recalculation interval and explicitly select the forecast horizon.
6. Enable **Set up recurring prediction calculation** and select the **5-point scale**.
7. If configuring the event's score name, use `previous_buyers_churn`.
8. 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](/docs/ai-hub/predictions/custom) 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
1. Go to **Behavioral Data Hub > Live Aggregates > Create aggregate**.
2. Select **Profile**.
3. Name the aggregate `Sessions in the last 3 months`. 
4. Select **Count**.
5. Select the `session.start` event, which is generated when a visitor starts a session.
6. Set the period to **Last 3 months**.
7. Save the aggregate.
8. Create a segmentation named `Repeat visitors without a recent bulky purchase`.
9. Require the session aggregate to be greater than or equal to `3`.
10. Add an **AND** condition: no `product.buy` matching 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.
11. Save the segmentation.


### Configure Propensity
1. Create a prediction using **Create from scratch > Propensity**.
2. Name it `Propensity to buy bulky items` and select the segmentation `Repeat visitors without a recent bulky purchase` as its audience.
3. In **Item feed**, select the configured feed and define an item filter for the bulky categories.
4. Review the matching items before saving the filter.
5. Enable recurring calculation, using the chosen interval, and keep the **5-point scale**.
6. 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](/docs/ai-hub/predictions/propensity) and the comparable [attribute-based propensity use case](/use-cases/propensity-attribute).

## 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:
1. Go to **Behavioral Data Hub > Live Aggregates > Create aggregate**.
2. Select **Profile** and the **Last** analysis.
3. Select the event, parameter, and event filter from the table.
4. Limit the relative period to the maximum score age defined for that model.
5. 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](/use-cases/decrease-sms-campaign-cost) shows the latest-score aggregate pattern. Use the event names above, which follow the model documentation.

### Create the audience segmentation
1. Go to **Decision Hub > Segmentations > New segmentation**.
2. Name it `Free shipping - risk audience`.
3. Add a condition: `Latest churn label` is `high` or `very high`.
4. Add an **OR** condition: `Latest bulky propensity label` is `very low`, `low`, or `medium`.
5. 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](/docs/predictions/predictions-concepts).

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.

1. Create a segmentation named `Previous bulky-item buyers` with a `product.buy` condition matching the selected categories. Set an explicit historical period suitable for your catalog and buying cycle.
2. Create a segmentation named `Recognized visitors active in the last 90 days`. Exclude `Previous bulky-item buyers` and the current `Free shipping - risk audience`.
3. Create a **Lookalikes** prediction. Select `Previous bulky-item buyers` as the source and `Recognized visitors active in the last 90 days` as the target.
4. Enable recurring calculation at the chosen interval and use the **5-point scale**.
5. Calculate the prediction and record its model ID.
6. Create an aggregate named `Latest bulky lookalike label` using **Last**, the `score_label` parameter of `snr.lookalike.score`, a filter for that model ID, and the chosen maximum score age.
7. Create a segmentation named `Free shipping - lookalike audience` for `high` or `very high` results. Reapply the exclusions of `Previous bulky-item buyers` and `Free shipping - risk audience` when 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](/docs/ai-hub/predictions/lookalikes).

## 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
1. Go to **Behavioral Data Hub > Live Aggregates > Create aggregate**.
2. Select **Profile**, name it `Latest basket products`, and select **Last**.
3. Select `cart.status` and its `products` parameter. In this example, each object contains the product ID in `sku` and its `quantity`.
4. Set the period to **Lifetime** and save the aggregate. 
5. Send an empty basket after checkout so this aggregate does not retain the completed order.
6. Use the product IDs and quantities from the same `cart.status` event. If your existing aggregate returns only `itemIds`, obtain the matching quantities from the same event and adapt the loop below.
7. In the Jinjava template, read the aggregate, iterate over the IDs, and use `catalogitemv2` to retrieve each product's price, margin, and bulky-item attributes from the feed-backed catalog. See [Insert usage](/developers/inserts/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](/developers/inserts/tag), [Jinjava filters](/developers/inserts/filter), and [Insert usage](/developers/inserts/insert-usage).


<details class="accordion"><summary>Check the Jinjava example</summary><div class="accordion-content"><pre><code class="language-jinja">{# 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 &gt; 0 and quantity &gt; 0 and margin_rate &gt;= 0 and margin_rate &lt;= 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 &gt; 0 and customer_shipping_charge &gt; 0 and basket_margin - delivery_cost &gt;= required_contribution_buffer %} &lt;p&gt;Your free shipping code: {% voucher assign=false %} FREE-SHIPPING-POOL-UUID {% endvoucher %} &lt;/p&gt; {% endif %} {% endaggregate %}</code></pre></div></details>


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
1. Send the complete `cart.status` after prices, quantities, or the delivery option change.
2. Render the content after the latest basket data is available to the aggregates.
3. In Jinjava, retrieve the products from the feed, calculate the contribution, and check the bulky-item and paid-delivery conditions.
4. Hide the outdated offer after a basket change and render the content again. Jinjava then recalculates the condition from the updated inputs.
5. Retrieve and display the voucher only inside the successful eligibility branch in the offer template.
6. Apply the store's voucher usage restrictions at checkout and record redemption for reporting.

The [voucher documentation](/docs/assets/code-pools) 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
---
1. Go to **Experience Hub > Dynamic content > Create new** and choose **Insert Object**.
2. Name the campaign `Free shipping for bulky items`.
3. In **Audience**, select `Free shipping - risk audience`, or `Free shipping - campaign audience` if you added Lookalikes.
4. In **Content**, select **Simple message**, set the CSS selector and insertion position near the order summary, and create the offer template.
5. Include the Jinjava example in the template and replace its placeholders. Add your banner design around the content inside the successful eligibility branch.
6. Target the basket page and set the campaign schedule. If using **On landing**, also implement the basket-change refresh described above.
7. 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](/developers/inserts/insert-usage) and [Creating dynamic content](/docs/campaign/dynamiccontent/creating-dynamic-content/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.

1. In the campaign's **Content** section, choose **A/B tests**.
2. 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.
3. Keep automatic allocation off during the comparison and use the same campaign audience and page targeting for both variants.
4. 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.
5. 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](/docs/campaign/dynamiccontent/creating-dynamic-content/creating-dynamic-content).


## Read more
---
- [Creating classification and regression predictions](/docs/ai-hub/predictions/custom)
- [Creating dynamic content](/docs/campaign/dynamiccontent/creating-dynamic-content/creating-dynamic-content)
- [Creating lookalikes predictions](/docs/ai-hub/predictions/lookalikes)
- [Creating propensity predictions](/docs/ai-hub/predictions/propensity)
- [Insert usage](/developers/inserts/insert-usage)
- [Jinjava filters](/developers/inserts/filter)
- [Jinjava tags](/developers/inserts/tag)
- [Tracking cart status](/developers/web/cart)
- [Voucher pools](/docs/assets/code-pools)