setInternalProperties is how you tell the cart they’re one thing.
How grouping works
The cart groups lines on two canonical properties. It doesn’t know your bundle app’s property names, so you translate: read whatever the app wrote, and stamp the canonical pair onto each line with a line transform.
These go through
setInternalProperties, not through Shopify. They’re a render-only overlay: they never reach properties, are never persisted to Shopify, and never appear on the order.
Step 1: find out what your app writes
Every bundle app names its properties differently, so start by looking at a real cart. Add a bundle on your storefront, then run this in the browser console:_) holding an ID, a reference, or the bundle’s name. Something like _bundle_id, _bundle_ref, or _parent_id is typical. Note the exact key, and whether one line is marked as the main product.
Step 2: map it onto the canonical properties
Paste into Cart settings → Custom script → Initialization, replacing the property names with the ones you found:If your app doesn’t mark a main product, leave
_aftersell_cart_bundle_role off entirely. The cart picks an anchor for you.What you get
Once lines are grouped, the anchor line carries abundle object and the drawer renders the bundle as a single item:
- Children nest under the anchor instead of appearing as separate rows.
- Quantity is atomic. Changing the bundle’s quantity scales every member together, using each child’s
perAnchorQtyratio, so a bundle with two of one component keeps that two-to-one relationship. - Removal is atomic. Removing the bundle removes every member line in one request, rather than leaving orphaned components behind.
- One price row. What it shows follows the bundle price setting on the Cart items block: the total of all members, or the main product’s price alone.
How the anchor is chosen
The anchor is the line the bundle displays as. The cart picks it in this order:- The line with
_aftersell_cart_bundle_roleset toparent. - Otherwise, the highest-priced member.
- Otherwise, the first member in the cart.
Rules worth knowing
- A bundle needs at least two lines. A single line carrying a bundle ID is left alone and renders normally.
- Shopify native bundles are already handled. Lines that Shopify itself marks as componentized are skipped by this grouping and adapted automatically. You only need this for apps that add separate lines.
- The transform runs on every render. Keep it cheap and free of side effects. Don’t call actions or fetch from inside it.
- Merging is additive. Your properties merge with any set by another transform. On a genuine conflict over the same key, the last-registered transform wins.
- Grouping runs after hiding and renaming, and before sorting. So a line you hide with
setHiddennever becomes part of a bundle, and a comparator sees the anchor, not the children.
Reading a bundle back
bundle.memberKeys, which holds the key of every member including the anchor.
Using it for other things
Bundle grouping is whatsetInternalProperties was built for, but the overlay is a general channel for render-only data you derive from a line. Anything you put there is readable at line.internalProperties and in a Custom code block, without touching the real cart:
properties[...] input on the product form so it reaches Shopify whoever performs the add.
Where to go next
registerLineTransform: the hook this runs through.- Cart object: the shape of
bundleand its children. - Cart items block: the bundle price setting.