A cart transform function is a Shopify Function that rewrites cart lines before checkout prices them. It runs on the cart.transform.run target and returns a list of operations: lineExpand splits one line into its components, linesMerge collapses several lines into a single bundle line, and lineUpdate overrides a line’s price, title or image.
Shopify’s reference states that changing the appearance of cart items, including titles, images and prices, is possible only through the Cart Transform API. That makes it the single place an app can rewrite what a buyer sees in the cart.
What it cannot do matters just as much. A function is pure, so it has no network access, no filesystem, no clock and no random numbers. Everything it decides has to come from the input query you write. And there is no operation for deleting a line, so “remove the free gift when the cart drops below £50” is not a job for this API.
What can a cart transform function actually change?
Three operations, and they behave differently.
lineExpand takes one cart line, usually a bundle parent, and replaces it with the components it contains. The buyer added one thing and sees several. linesMerge does the reverse: several component lines collapse into one line that represents a bundle, priced as a unit. lineUpdate leaves the line where it is and overrides its presentation.
That last one carries a plan restriction. Only development stores and stores on Shopify Plus can use apps with lineUpdate operations, so a merchandising trick built on it will not ship to a standard-plan client. Expand and merge have no such limit.
Pricing on any of them goes through the same two shapes, fixedPricePerUnit for an absolute amount and percentageDecrease for a proportion. The input gives you presentmentCurrencyRate, which you multiply by any hardcoded amount so the maths survives a currency switch.
Two more edges before you design around this. The maximum quantity on an expanded or merged line is 2000. And if a selling plan is present on the line, Shopify rejects lineExpand, linesMerge and lineUpdate outright, which rules out transforming subscription lines.
How do you write and register a cart transform function?
Scaffold it with the CLI. The template name is the API name in snake case.
shopify app generate extension --template cart_transform --name bundle-transform
Pick Rust, JavaScript, TypeScript or Wasm at the prompt. Shopify defaults to Rust and recommends it for staying inside the platform limits, which the instruction ceiling further down makes concrete.
Next, the input query. Your function sees only the fields you ask for, so this file is the whole world the logic gets. This one reads a list of component variant IDs off a metafield on the parent variant, following Shopify’s customized bundle tutorial:
query Input {
cart {
lines {
id
quantity
merchandise {
__typename
... on ProductVariant {
id
product {
title
}
componentReference: metafield(namespace: "custom", key: "component_reference") {
jsonValue
}
}
}
}
}
}
The __typename on merchandise is not optional. It is a union, and without it the generated types will not compile.
Then the logic. The export name has to match the target, so a cart.transform.run target wants cartTransformRun:
export function cartTransformRun(input) {
const operations = [];
for (const line of input.cart.lines) {
if (line.merchandise.__typename !== "ProductVariant") continue;
const components = line.merchandise.componentReference?.jsonValue;
if (!Array.isArray(components) || components.length === 0) continue;
operations.push({
lineExpand: {
cartLineId: line.id,
title: line.merchandise.product.title,
expandedCartItems: components.map((merchandiseId) => ({
merchandiseId,
quantity: 1,
})),
},
});
}
return { operations };
}
Registration is the step people miss. Deploying the extension does not activate it. You activate it once per store with an Admin GraphQL mutation, and the app needs the write_cart_transforms scope:
mutation {
cartTransformCreate(functionHandle: "bundle-transform", blockOnFailure: false) {
cartTransform {
id
}
userErrors {
field
message
}
}
}
Pass functionHandle, not functionId. The mutation reference marks functionId deprecated, and the handle saves you a round trip to look an ID up. If this comes back with Could not find Function, the docs point at three causes: shopify app dev is not running, the handle is wrong, or the scope is missing.
blockOnFailure decides what a crash costs you. Left at its default of false, a function that throws is skipped and the cart proceeds untransformed, which means a bundle silently sells at component prices. Set it to true and the cart operation fails instead. Neither outcome is good, so choose the one you can detect. A silently mispriced bundle is harder to spot than a cart that refuses to update.
On the storefront side, a failed run used to surface as the generic INVALID code with the message “An error occurred in your cart”. Since API version 2026-04, the Storefront API returns MERCHANDISE_LINE_TRANSFORMERS_RUN_ERROR instead, so a headless build can tell a transform failure apart from a validation error and say something useful.
What does the parent product need?
For an expand, the parent variant is a real variant that the buyer adds to the cart. If it should never be sold on its own, set requiresComponents to true on it. Component IDs live in a metafield, and the tutorial defines one of type list.variant_reference with the key component_reference on PRODUCTVARIANT.
Shopify’s bundles documentation draws a line between fixed bundles, where the components are assigned to the product, and customized bundles, which is what a function gives you. Fixed bundles cover the simple cases without any code. Reach for a function when the combination is decided in the cart rather than in the catalogue.
The limits on bundles themselves are firm: up to 150 components, up to three options, no nesting, and a product cannot both have components and be a component of something else. One more clause is easy to trip over in a multi-app store. Once an app has assigned components to a bundle, only that app can manage them.
What breaks once other apps are installed?
An app can install at most one cart transform function per store, but a store can have several apps that each install one, and all of them run.
When two operations target the same cart line, Shopify combines every installed function’s output into one ordered list and picks a subset. The ordering is by the time each app activated its function through cartTransformCreate, so the earliest-activated app wins ties. Within that list: the first lineExpand for a line executes and the rest are discarded, the same for linesMerge and lineUpdate, an expand beats a merge on the same line, and a fixed expand beats a customized one.
The practical reading is that your function’s correctness partly depends on install order in a store you do not control, so touch as few lines as you can.
Then there are the resource limits, which are per execution and published in the Functions reference. For carts up to 200 line items: 11 million instructions, 128 kB of input and 20 kB of output. Above 200 lines those scale proportionally. The input query has its own budget, capped at 3000 bytes excluding comments and a calculated query cost of 30, where a metafield selection costs 3 and a plain leaf costs 1. Metafield values over 10,000 bytes are not returned at all. The 20 kB output ceiling is the one that bites bundle work, because a cart full of expanded lines produces a lot of JSON.
Weighing this against a discount is the first question to settle, and our guide to migrating to the Discount Function API covers where that line sits. Configuration belongs in a metafield or metaobject, not compiled into the function, and we compared the two custom data models previously.
Use a cart transform function when the thing you are selling is genuinely assembled in the cart and Shopify’s fixed bundles cannot express it. Build it in Rust if the cart is large or the logic loops, keep the input query narrow, and make blockOnFailure a deliberate decision. If you only need a price to move, use a discount function. If you only need a different title in the cart, check the client is on Plus before you promise it, because lineUpdate will not run otherwise.
Whoooop builds Shopify apps and functions for merchants whose catalogue does not fit the standard product model, including bundle and kit logic that has to hold up at checkout. If you are scoping one, our Shopify development work is the place to start.