# Expand a bundle into its components

> A Shopify cart transform Function that expands a bundle product into its component variants in the cart, using the standard component metafields.

Page: https://lintelagency.com/resources/shopify-functions/expand-bundle-into-components

## In short

This cart transform Function turns a bundle variant into its component items in the cart and checkout, so inventory and fulfillment use the real products. Components come from the custom.component_reference and custom.component_quantities variant metafields. It uses the lineExpand operation of the cart.transform.run target, Cart Transform API version 2026-10, available on any plan in a public app.

## Facts

- **Function API:** Cart Transform
- **Target:** cart.transform.run
- **Operation:** lineExpand
- **Plan:** Any plan in a public app; Shopify Plus in a custom app
- **App scope:** write_cart_transforms
- **API version:** 2026-10
- **Last checked:** 2 November 2026

## How to use it

1. Create variant metafield definitions custom.component_reference (list of product variants) and custom.component_quantities (list of integers).
2. On each bundle variant, list the component variants and their quantities in the same order.
3. Create an app with the Shopify CLI, add the extension (shopify app generate extension --template cart_transform) and replace its files with the files below.
4. Run the fixtures with shopify app function run, deploy, and activate the Function with cartTransformCreate.

## Code

### shopify.extension.toml

```toml
api_version = "2026-10"

[[extensions]]
name = "Expand a bundle into its components"
handle = "expand-bundle-components"
type = "function"

  [[extensions.targeting]]
  target = "cart.transform.run"
  input_query = "src/cart_transform_run.graphql"
  export = "cart-transform-run"

  [extensions.build]
  command = ""
  path = "dist/function.wasm"
```

### src/cart_transform_run.graphql

```graphql
query CartTransformRunInput {
  cart {
    lines {
      id
      merchandise {
        __typename
        ... on ProductVariant {
          componentReference: metafield(namespace: "custom", key: "component_reference") {
            jsonValue
          }
          componentQuantities: metafield(namespace: "custom", key: "component_quantities") {
            jsonValue
          }
        }
      }
    }
  }
}
```

### src/cart_transform_run.js

```js
// @ts-check

/**
 * @typedef {import("../generated/api").CartTransformRunInput} CartTransformRunInput
 * @typedef {import("../generated/api").CartTransformRunResult} CartTransformRunResult
 */

/**
 * The bundle variant lists its components in two variant metafields, the
 * definitions Shopify's bundle guide uses: custom.component_reference (list of
 * variants) and custom.component_quantities (list of integers, same order).
 * Quantities are for one bundle.
 */

/**
 * @param {CartTransformRunInput["cart"]["lines"][number]} line
 * @returns {CartTransformRunResult["operations"][number] | null}
 */
function expandOperation(line) {
  if (line.merchandise.__typename !== "ProductVariant") return null;
  const variantIds = line.merchandise.componentReference?.jsonValue;
  const quantities = line.merchandise.componentQuantities?.jsonValue;
  if (!Array.isArray(variantIds) || variantIds.length === 0) return null;
  // Mismatched lists mean a half-edited bundle; leave the line as it is
  // rather than expand it into the wrong items.
  if (!Array.isArray(quantities) || quantities.length !== variantIds.length) return null;

  // No price on the items: Shopify splits the bundle price across the
  // components by their own prices (its weight price algorithm).
  return {
    lineExpand: {
      cartLineId: line.id,
      expandedCartItems: variantIds.map((merchandiseId, index) => ({ merchandiseId, quantity: quantities[index] })),
    },
  };
}

/**
 * @param {CartTransformRunInput} input
 * @returns {CartTransformRunResult}
 */
export function cartTransformRun(input) {
  const operations = input.cart.lines.map(expandOperation).filter((operation) => operation !== null);
  return { operations };
}
```

## Test input and expected output

### tests/bundle-line.input.json

```json
{
  "cart": {
    "lines": [
      {
        "id": "gid://shopify/CartLine/1",
        "merchandise": {
          "__typename": "ProductVariant",
          "componentReference": {
            "jsonValue": ["gid://shopify/ProductVariant/1111111111", "gid://shopify/ProductVariant/2222222222"]
          },
          "componentQuantities": { "jsonValue": [2, 1] }
        }
      },
      {
        "id": "gid://shopify/CartLine/2",
        "merchandise": { "__typename": "ProductVariant", "componentReference": null, "componentQuantities": null }
      }
    ]
  }
}
```

### tests/bundle-line.output.json

```json
{
  "operations": [
    {
      "lineExpand": {
        "cartLineId": "gid://shopify/CartLine/1",
        "expandedCartItems": [
          { "merchandiseId": "gid://shopify/ProductVariant/1111111111", "quantity": 2 },
          { "merchandiseId": "gid://shopify/ProductVariant/2222222222", "quantity": 1 }
        ]
      }
    }
  ]
}
```

### tests/mismatched-lists.input.json

```json
{
  "cart": {
    "lines": [
      {
        "id": "gid://shopify/CartLine/1",
        "merchandise": {
          "__typename": "ProductVariant",
          "componentReference": {
            "jsonValue": ["gid://shopify/ProductVariant/1111111111", "gid://shopify/ProductVariant/2222222222"]
          },
          "componentQuantities": { "jsonValue": [2] }
        }
      }
    ]
  }
}
```

### tests/mismatched-lists.output.json

```json
{ "operations": [] }
```

### tests/no-bundles.input.json

```json
{
  "cart": {
    "lines": [
      { "id": "gid://shopify/CartLine/1", "merchandise": { "__typename": "ProductVariant", "componentReference": null, "componentQuantities": null } }
    ]
  }
}
```

### tests/no-bundles.output.json

```json
{ "operations": [] }
```

## Limits and edge cases

- No component prices are sent, so Shopify splits the bundle price across the components in proportion to their own prices.
- Shopify rejects cart transform operations when a selling plan (subscription, pre-order) is on the line.
- An expanded line can have at most 150 components.
- One cart transform Function per app per store; if two operations target the same line, lineExpand wins.

## Questions

### How do product bundles work with Shopify Functions?

A cart transform Function changes how cart lines are presented: lineExpand turns one bundle line into components, linesMerge turns components into one bundle line. Orders keep the component lines with a reference to the bundle parent.

### Do I need Shopify Plus for bundles?

Not for lineExpand and linesMerge in a public app. Only lineUpdate (changing a line’s price, title or image) needs Shopify Plus or a development store.

### Why doesn’t the cart transform activate?

cartTransformCreate needs the write_cart_transforms scope, and the store must have upgraded to Checkout Extensibility.

## Shopify documentation

- [Cart Transform Function API](https://shopify.dev/docs/api/functions/latest/cart-transform)
- [Create a bundle app](https://shopify.dev/docs/apps/build/product-merchandising/bundles/create-bundle-app)

## Related

- [Bundles with Cart Transform, and what needs Plus](https://lintelagency.com/journal/shopify-bundles-cart-transform)
- [Merge cart lines into a bundle](https://lintelagency.com/resources/shopify-functions/merge-lines-into-bundle)
- [Product configurators on Shopify: app, theme or custom](https://lintelagency.com/journal/product-configurator-shopify)

License: MIT. Free to use in your own store and in client work.
