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.
- 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
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.
How to use it
- Create variant metafield definitions custom.component_reference (list of product variants) and custom.component_quantities (list of integers).
- On each bundle variant, list the component variants and their quantities in the same order.
- 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.
- Run the fixtures with shopify app function run, deploy, and activate the Function with cartTransformCreate.
Code
shopify.extension.tomlapi_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.graphqlquery 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// @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{
"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{
"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{
"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{ "operations": [] }
tests/no-bundles.input.json{
"cart": {
"lines": [
{ "id": "gid://shopify/CartLine/1", "merchandise": { "__typename": "ProductVariant", "componentReference": null, "componentQuantities": null } }
]
}
}
tests/no-bundles.output.json{ "operations": [] }
Written by Lintel. MIT license: free to use in your own store and in client work.
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
- Written for Function API version 2026-10 and checked against Shopify’s reference on the date shown. Shopify releases a new version every quarter.
- Custom apps that contain Functions only run on Shopify Plus stores; a public app with the same Function runs on any plan unless the page says otherwise.
- Every example ships with test fixtures, and we run them before publishing. Run them again after you change the code: shopify app function run.
- MIT license. Use the code in your own store or in client work.
Need this built, tested and maintained for your store? See Shopify Functions development.
