Subscription Bundles on Shopify: How Flex Bundles Works with Selling Plans

TLDR

  • Shopify's Cart Transform doesn't expand lines that carry a selling plan, so a subscribed bundle checks out as one parent line
  • Flex Bundles doesn't manage subscriptions. Keep Shopify Subscriptions, Recharge, or Skio for billing, and Flex Bundles expands the bundle on the order after checkout, and again on every renewal
  • Each subscriber gets one subscription contract for the whole bundle, not one per component, so skips, pauses, and cancellations apply to the bundle as a unit
  • Fixed bundles need no code: put a selling plan on the parent product and turn on Expand bundle on order edits
  • Build-your-own bundles send selling_plan alongside _components in the Cart API request, with either pricing mode
  • The customer pays the parent price less the plan's discount, and the components are priced to add up to exactly that

Subscribe and save is one of the oldest bundle plays there is: a monthly coffee box, a skincare routine delivered every eight weeks, a replacement pair of glasses every year. It is also where most Shopify bundle setups fall over, because Shopify's bundle engine and Shopify's subscription engine don't talk to each other.

This post explains the gap, how Flex Bundles closes it without becoming a subscription app, and the exact code you need for fixed and build-your-own bundles.

The gap: Cart Transform skips subscription lines

Every modern Shopify bundle, Flex Bundles included, is built on the Cart Transform Function. You add one parent product to the cart, and at checkout the function expands it into the real component line items: accurate prices, inventory decremented per component, and a pick list your warehouse can use.

Subscriptions are built on selling plans. A selling plan (Deliver every month, 10% off) is attached to a product, and a cart line that carries one becomes a subscription at checkout. Your subscription app then bills that subscription and creates a new order on every renewal.

The two don't combine. Shopify doesn't run Cart Transform operations on cart lines that have a selling plan. A subscribed bundle stays as one parent line through checkout, and every renewal order is created from that same parent line. Without anything else in place, the warehouse receives a SKU called "Build Your Glasses" and nothing to pick.

It's the reason subscription bundles have been one of the standing Shopify bundle limitations.

How Flex Bundles handles it

Flex Bundles doesn't try to be a subscription app. Billing, renewals, skips, failed payments, and the customer portal stay with the app you already use: Shopify Subscriptions, Recharge, Skio, or anything else that sells through selling plans. Flex Bundles takes care of the bundle.

The flow, for a first order and every renewal after it:

  1. The storefront adds the parent product with a selling plan. For a build-your-own bundle, the same request carries the customer's picks in _components.
  2. Checkout charges the subscription price. The line stays as the parent product and costs the parent variant's price less the plan's discount.
  3. The subscription app creates the subscription contract. The contract line is the parent product, with its line properties.
  4. The order is created, and Flex Bundles expands it. Shortly after checkout, an order edit adds each component, removes and restocks the parent, and discounts the components so they add up to exactly what the customer paid.
  5. Each renewal repeats step 4. The subscription app creates a new order from the contract, and Flex Bundles expands it the same way.

This is the same post-purchase expansion that already handles bundles sold on the post-purchase page and orders from channels that skip Cart Transform. Subscriptions are one more case where the bundle arrives on the order unexpanded.

One subscription for the whole bundle

Because the subscription is on the parent, each subscriber has one subscription contract for the bundle, not one per component. A four-item box is one subscription, not four running side by side. That matters everywhere the subscription is touched after checkout:

  • The customer manages one item. The portal shows one subscription, and skipping, pausing, changing the frequency, or cancelling applies to the whole bundle. A box can't drift into a partial bundle because one component was cancelled and the rest kept renewing.
  • There's one price to manage. The contract carries one price and one plan discount, instead of several component prices that each need the discount and can each change on their own.
  • Each renewal is one order. Every cycle produces a single order with the whole bundle on it, which Flex Bundles expands into the components your warehouse picks.
  • Reporting stays at the bundle level. Subscriber counts, churn, and lifetime value are measured per bundle in your subscription app, not split across component SKUs.

The components still reach the order, the inventory, and the pick list. They just don't each become a subscription of their own.

Fixed bundles: no code

A fixed bundle has its components attached to the parent variant, so nothing about the customer's choice needs to travel with the order. Setup is three steps:

  1. In Flex Bundles, open the fixed bundle and turn on Expand bundle on order edits.
  2. In your subscription app, add a selling plan to the bundle's parent product. In Shopify Subscriptions that's a subscription plan with the parent product selected.
  3. On the product page, add your subscription app's widget (Shopify Subscriptions ships an app block). It adds a selling_plan field to the product form, and the normal Add to cart button does the rest.

If you add to cart with your own code instead, include the selling plan id on the item:

await fetch(`${window.Shopify.routes.root}cart/add.js`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    items: [
      {
        id: 52362406822189,          // the fixed bundle's parent variant
        quantity: 1,
        selling_plan: 689596760365,  // the subscription plan
      },
    ],
  }),
});

That's it. Here is a real test order: The Weekend Kit, a four-item fixed bundle at $100, sold on a monthly plan at 10% off.

Line Price Bundle discount Quantity
The Weekend Kit (parent, removed and restocked) $90.00 0
Ocean Blue Shirt $50.00 -$30.00 1
Navy Sports Jacket $60.00 -$36.00 1
Chequered Red Shirt $50.00 -$30.00 1
Zipped Jacket, Blue $65.00 -$39.00 1
Order total $90.00

The customer paid $90. The components retail at $225, so each one carries a 60% "Bundle discount" and the order still totals $90, with nothing owed either way. The order is tagged flex-bundles-expanded alongside the subscription app's own Subscription tags.

Build-your-own bundles

A flex bundle is built by the customer, so the components come from the storefront in the _components line property. With a subscription, that property rides on the parent line through checkout and onto the subscription contract, and Flex Bundles reads it from the order to expand the bundle.

In the flex bundle editor, turn on Expand bundle on order edits. It works with either pricing mode, Parent Price or Component Sum.

Subscriptions are charged the parent price. Component Sum prices a bundle inside Cart Transform: the function reads each component's price from _components and sets the line to their total at checkout. Shopify doesn't run Cart Transform on lines with a selling plan, so on a subscription that step never happens. Checkout charges the parent variant's own price less the plan's discount, whatever the customer picked, and every renewal is billed from that same price. Expanding the order afterwards splits what was actually paid across the components. A Component Sum bundle still prices one-time purchases from its components; set its parent price to what a subscription should cost.

On the storefront, three things change compared with a one-time build-your-own bundle.

1. Show the parent's selling plans

The plans belong to the parent product, the one you add to the cart, not to the components. In Liquid, each parent variant lists its plans and their prices in selling_plan_allocations:

{%- assign parent = all_products['build-your-glasses'] -%}
{%- assign parent_variant = parent.selected_or_first_available_variant -%}

{%- if parent_variant.selling_plan_allocations.size > 0 -%}
  <fieldset class="purchase-options">
    <legend>Purchase option</legend>

    {%- unless parent.requires_selling_plan -%}
      <label>
        <input type="radio" name="selling_plan" value="" checked>
        One-time purchase, {{ parent_variant.price | money }}
      </label>
    {%- endunless -%}

    {%- for allocation in parent_variant.selling_plan_allocations -%}
      <label>
        <input
          type="radio"
          name="selling_plan"
          value="{{ allocation.selling_plan.id }}"
          {% if parent.requires_selling_plan and forloop.first %}checked{% endif %}
        >
        {{ allocation.selling_plan.name }}, {{ allocation.price | money }}
      </label>
    {%- endfor -%}
  </fieldset>
{%- endif -%}

allocation.price is the subscription price with the plan's discount already applied, so you can show it as is. Outside Liquid, the same data is on /products/<handle>.js under selling_plan_groups, and in the Storefront API under sellingPlanAllocations.

2. Send the selling plan with _components

The Cart API request is the one you already send for a flex bundle, plus selling_plan on the item:

const components = [
  { id: 67577705824557, quantity: 1, attributes: { Color: 'Tortoise' } },
  { id: 67577706119469, quantity: 1, attributes: { 'Right (OD) SPH': '+3.75', 'Left (OS) SPH': '+3.75' } },
];

const plan = document.querySelector('input[name="selling_plan"]:checked')?.value;

const item = {
  id: 67577705791789, // the parent variant (Build Your Glasses)
  quantity: 1,
  properties: {
    _components: JSON.stringify(components),
    _settings: JSON.stringify({ title: 'Ansel' }),
  },
};

if (plan) {
  item.selling_plan = Number(plan);
}

await fetch(`${window.Shopify.routes.root}cart/add.js`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ items: [item] }),
});

On a subscription line the price comes from the parent variant and the plan, so _discount and any component price values don't change the charge. A Component Sum bundle can keep sending price for its one-time purchases.

3. List the picks on the cart line

A one-time bundle shows its components in the cart, because Cart Transform has already expanded it. A subscribed bundle doesn't expand until after checkout, so the cart and checkout show only the parent line. Add the customer's picks as visible properties so they can see what they are subscribing to. Properties without a leading underscore are shown in the cart, at checkout, and on the order:

if (plan) {
  item.selling_plan = Number(plan);
  item.properties['Frame'] = 'Ansel, Tortoise';
  item.properties['Lens'] = 'Single Vision';
  item.properties['Prescription'] = 'R +3.75 / L +3.75';
}

The cart line then reads "Build Your Glasses", the plan name (Deliver every week, 10% off), and the three picks underneath. Most themes already render line properties and the selling plan name; if yours hides them, check the cart line item snippet for item.properties and item.selling_plan_allocation.

What the order looks like

Here is a real order from our demo store, with the parent at $150 and a weekly plan at 10% off:

Line Price Bundle discount Quantity
Build Your Glasses (parent, removed and restocked) $135.00 0
Ansel, Tortoise $185.00 -$95.81 1
Prescription Lens, Single Vision $95.00 -$49.19 1
Order total $135.00

The customer paid $135: the $150 parent less 10%. The frame and lens retail at $280, so Flex Bundles spreads a $145 bundle discount across them in proportion to their prices, and the order still totals exactly $135.

The parent line stays on the order with a quantity of zero, which is where its properties live. Shopify's order editing can add products to an order but can't attach properties to them, so per-component details such as the prescription stay readable on the parent line, in the visible properties you added and in _components:

{
  "name": "Build Your Glasses",
  "current_quantity": 0,
  "properties": [
    { "name": "_components", "value": "[{\"id\":67577705824557,\"quantity\":1,\"attributes\":{\"Color\":\"Tortoise\"}},{\"id\":67577706119469,\"quantity\":1,\"attributes\":{\"Right (OD) SPH\":\"+3.75\",\"Left (OS) SPH\":\"+3.75\"}}]" },
    { "name": "Frame", "value": "Ansel, Tortoise" },
    { "name": "Lens", "value": "Single Vision" },
    { "name": "Prescription", "value": "R +3.75 / L +3.75" }
  ]
}

If your 3PL or ERP needs those details per component, map them from the parent line. The bundle structure itself is also written to the $app:bundle_breakdown order metafield, as it is for every bundle order; Where Shopify Bundle Data Lives covers it.

How pricing works

On a subscription, the customer always pays the parent variant's price less the selling plan's discount. Flex Bundles doesn't change what was charged; it spreads that amount across the components:

  • The discount is proportional. Each component's share of the bundle discount matches its share of the components' total retail price, rounded to the cent, with any leftover cent placed so the total is exact.
  • Discount codes and tax are accounted for. If an order-wide code applied at checkout, or per-line tax rounding would leave the order a cent off, Flex Bundles adjusts the component discounts so the order still balances.
  • The plan's discount is already in the price. The selling plan discount is part of what the customer paid, so it flows through to the components automatically.

This makes the parent price the one number that matters. Set it to what the subscription should cost before the plan's discount, and make sure the components' combined retail price is at least that much, so the expansion discounts them down rather than leaving the order short.

When the price should depend on the picks

A single parent variant means a single price, whatever the customer picks. If a subscription to a $240 frame and a $95 lens should cost more than a $185 frame and the same lens, give the parent product one variant per price point ($280, $335, and so on) and add the variant whose price matches the customer's selection. Put the selling plan on every variant. Each subscription then charges what the one-time bundle would have, less the plan's discount.

Building the storefront with AI

The storefront changes above are small and well defined, which makes them a good job for an AI coding assistant. A prompt that works:

In this Shopify theme, extend the bundle builder block so customers can subscribe.

- Read the selling plans from the PARENT product's selected variant
  (selling_plan_allocations), not from the component products.
- Render "One-time purchase" plus one radio per plan, showing allocation.price.
  Hide one-time if product.requires_selling_plan.
- On add to cart, keep the existing _components payload. When a plan is selected,
  add selling_plan (number) to the item, and add visible properties listing the
  customer's picks (e.g. Frame, Lens), because the cart won't show components
  for a subscribed line.
- The total should show the selected plan's price, otherwise the parent price.

Then test with a real order before you launch: check that the cart shows the plan and the picks, that the order is tagged flex-bundles-expanded shortly after checkout, and that the components add up to what was paid.

Limits to know

  • Subscriptions are charged the parent price. Component Sum and _discount are applied by Cart Transform, which doesn't run on subscription lines, so a Component Sum bundle's subscription costs the parent price less the plan's discount.
  • Customers can't swap components in the subscription portal. Portals change quantities and variants of the subscribed product; the picks live in _components. A new selection means a new subscription.
  • Component properties stay on the parent line. Order editing can't attach properties to the components it adds.
  • Store currency only. Orders placed in a currency other than your store's currency are left unexpanded rather than risk a pricing mismatch after conversion.
  • Expansion needs the bundle to be active. Deactivating a bundle in Flex Bundles stops its subscription orders from expanding, including renewals.

Why not build subscriptions into the bundle app

Billing is the hard part of subscriptions, and it's a solved problem: retries, dunning, portals, skips, and churn tooling are what Shopify Subscriptions, Recharge, and Skio are for, and most high-volume brands already run one of them. What those apps can't do is turn a bundle into a pick list, because Shopify won't run Cart Transform on their lines. Flex Bundles fills exactly that gap and leaves the rest to the app you already trust.

If you sell bundles on subscription today and your warehouse is picking from a parent SKU, turn on Expand bundle on order edits for those bundles. If you're choosing a bundle app and subscriptions matter, install Flex Bundles and run one subscribed order through your store to see it end to end.

Frequently Asked Questions

Does Flex Bundles replace my subscription app?

No. Billing, renewals, skips, pauses, failed payments, and the customer portal stay with your subscription app: Shopify Subscriptions, Recharge, Skio, or any app that sells through Shopify selling plans. Flex Bundles only makes sure the bundle on each order is broken out into the components your warehouse picks.

Why doesn't the bundle expand at checkout like a one-time purchase?

Shopify doesn't run Cart Transform operations on cart lines that have a selling plan. The subscribed line stays as the parent product through checkout. Flex Bundles expands it as soon as the order is created, using an order edit, which is why the setting is called Expand bundle on order edits.

Do renewal orders expand too?

Yes. Each renewal is a new order containing the parent product, created by your subscription app from the subscription contract. Flex Bundles expands it the same way as the first order. For build-your-own bundles, the customer's selections travel on the line's _components property, which the subscription contract keeps for every renewal.

Can subscribers change what's in their box between deliveries?

Not through the subscription portal today. Portals edit quantities and variants of the subscribed product, and the customer's picks live in a line property they can't edit. To change the contents, the customer starts a new subscription with the new selection.