How Flex Bundles Work in Shopify

TLDR

  • A flex bundle is customer-built: mix-and-match, build-your-own, or dynamic bundles
  • The bundle configuration is passed to the Cart Transform Function with the add-to-cart request, in the _components line item property
  • A metafield on the parent product marks it as a flex bundle parent and holds its settings and rules
  • Pricing is either Parent Price (with an optional _discount) or Component Sum, and server-side rules validate every payload

This is a follow-up to How Native Bundles Work in Shopify, which covers the foundation: every bundle is a parent plus components, the parent carries a configuration, and the Cart Transform Function expands the parent into its components on every cart event.

A flex bundle is the customer-built version. You'll also hear it called a dynamic bundle, a mix-and-match bundle, or BYOB (build your own box). The merchant sets the boundaries; the customer picks what goes inside.

That changes one key thing compared to a fixed bundle: the configuration can't be stored ahead of time, because nobody knows the components until the customer chooses. Instead, the configuration is passed to the Cart Transform Function with the add-to-cart request, in the _components line item property.

The configuration: _components

Your storefront's bundle builder adds the parent variant to cart, with the customer's choices packed into a line item property called _components. The most common way to do that is a request to Shopify's Cart API:

await fetch(window.Shopify.routes.root + 'cart/add.js', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    items: [{
      id: 51618980823341, // parent variant
      quantity: 1,
      properties: {
        _components: JSON.stringify([
          { id: 12345678901, quantity: 2, attributes: { "Size": "Medium" } },
          { id: 12345678902, quantity: 1, attributes: { "Engraving": "JKS" } },
          { id: 12345678903, quantity: 1 }
        ]),
        // Visible to the customer in cart and checkout
        "Item 1": "Ocean Blue Shirt (M) x 2",
        "Item 2": "Navy Sports Jacket, engraved",
        "Item 3": "Zipped Jacket"
      }
    }]
  })
});

Each entry in _components is a selected variant id, a quantity, and optional attributes that land on that component's line item in the order (a size, an engraving, a gift message). Properties that start with an underscore are hidden from the customer; properties without one, like "Item 1", are shown in cart and checkout.

Fun fact

You don't need the Cart API.

_components doesn't have to be sent with a Cart API request. It's a regular line item property, so Shopify's default product form can carry it too. Put a hidden properties[_components] input in the form, and have your builder write the JSON into it as the customer makes selections:

{% form 'product', product %}
  <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
  <input type="hidden" name="properties[_components]" id="bundle-components">
  <!-- your selection UI -->
  <button type="submit" id="bundle-submit" disabled>Add to cart</button>
{% endform %}

<script>
  // Call this whenever the customer's selections change
  function updateBundle(selections) {
    document.getElementById('bundle-components').value = JSON.stringify(selections);
    document.getElementById('bundle-submit').disabled = selections.length === 0;
  }
</script>

When the form submits, Shopify adds the parent variant with _components attached, exactly as if it had come from /cart/add.js. It's a less common setup, but it's useful when you want to keep a theme's existing add-to-cart behavior (its cart drawer, its analytics events) and only add a selection UI on top.

Keep the submit button disabled until there's a valid selection. If the form submits with an empty or malformed _components, the Cart Transform won't expand the bundle, and the parent is added on its own at the parent variant's price.

The parent product's metafield

The parent product still carries an app-owned metafield, $app:flex_config, but it doesn't hold a component list. It tells the Cart Transform Function "this product is a flex bundle parent" and carries everything the merchant controls:

  • Whether the bundle is active
  • Display settings, like a custom title and image for cart and checkout
  • Rules that every submitted _components payload must pass (more on those below)

How a flex bundle gets expanded

  1. The customer builds their bundle in your UI and adds it to cart.
  2. On the cart event, Shopify runs the Cart Transform Function.
  3. The function sees the parent product has an active flex_config.
  4. It reads _components from the cart line and checks the payload against the bundle's rules.
  5. If everything passes, it expands the parent into the selected components, with their quantities, attributes, and prices.

Because the product is the parent and the selections ride on the cart line, one parent can produce any number of different bundles. Two customers can add the same parent and check out with completely different contents.

Pricing: Parent Price or Component Sum

Flex bundles support two pricing modes, chosen by what your storefront sends:

  • Parent Price. The bundle costs the parent variant's price. Add an optional _discount property (a percentage, e.g. 15 for 15% off) to discount it. Good for "any 3 for $60" style offers where the price doesn't depend on what's picked.
  • Component Sum. Include a price on every component in _components, and the bundle costs the sum of those prices times their quantities. Good for build-your-own boxes where the total grows with each item.

Component Sum is used only when every component has a price. Otherwise the bundle falls back to Parent Price. On multi-currency stores, send _currency and _currency_rate alongside Component Sum prices so they're converted correctly at checkout. The Cart API integration docs cover the details.

Rules keep submitted prices honest

In Component Sum mode, prices come from the browser. That means a hand-crafted request could try to submit prices your storefront never offered. You don't have to trust them. Each flex bundle can carry server-side rules in its product metafield:

  • Minimum bundle price, checked after any _discount
  • Component price floor, with an option to allow a limited number of free components
  • Component counts, such as "between 3 and 6 items" or "no more than 2 of any one item"
  • Eligible products, restricting which variants the bundle accepts
  • Maximum line quantity for the bundle itself

If a payload breaks a rule, the Cart Transform declines to expand it, so the line falls back to the parent variant's real price, and a checkout validation blocks the order with an error naming the broken rule. See Validating Submitted Prices.

What the order looks like

Just like a fixed bundle, the parent disappears on the order. The customer's selected components are real line items with their own SKUs, inventory, and prices, grouped under a "Part of" label in the Shopify admin. Each one carries the attributes you passed at add-to-cart plus a _flex_bundle property referencing the bundle, so fulfillment knows exactly what to pack for that customer.

When to use a flex bundle

Flex bundles are the right choice when the customer decides what goes in the box: build-your-own boxes, mix-and-match sets, variety packs, personalized kits. They take more work than a fixed bundle because you build the selection UI, but that's the point: you control the experience, the logic, the data that flows to checkout, and the pricing.

If you're scoping a custom bundle builder, book a demo and we'll walk through the integration with your products.

Frequently Asked Questions

Where is a flex bundle's configuration stored?

It isn't stored ahead of time. The configuration (the selected components) is passed to the Cart Transform Function in the _components line item property when the bundle is added to cart. An app-owned metafield on the parent product ($app:flex_config) marks the product as a flex bundle parent and holds its settings and rules.

How are the customer's selections sent to Shopify?

Your storefront adds the parent variant to cart with a _components line item property: a JSON-stringified array of the selected variant ids and quantities, plus optional prices and per-component attributes. Most builders send it with a Cart API request to /cart/add.js, but a standard Shopify product form works too, with _components in a hidden properties[_components] input. The Cart Transform Function reads it and expands the bundle.

Can customers tamper with flex bundle prices?

In Component Sum mode the prices come from the browser, so every flex bundle supports server-side rules such as a minimum bundle price and a per-component price floor. A payload that breaks a rule is not expanded, the line falls back to the parent variant's real price, and checkout is blocked with an error.

Do flex bundles need custom theme code?

Yes. The selection experience (the builder UI where customers pick their items) is built in your theme or headless storefront, and it sends the add-to-cart request with _components. That's what gives you full control over the UX.