Skip to main content
Most bundle apps build a bundle by adding each component as its own cart line, then linking them with line item properties of their own design. Shopify’s Ajax API hands those lines to the cart with no indication that they belong together, so by default the drawer shows a three-part bundle as three unrelated items, each with its own price and quantity stepper. 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:
You’re looking for a property shared across the bundle’s lines. It’s usually a hidden property (the name starts with _) 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:
That’s the whole integration. Once two or more lines share an ID, the cart folds them into one bundle.
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 a bundle 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 perAnchorQty ratio, 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:
  1. The line with _aftersell_cart_bundle_role set to parent.
  2. Otherwise, the highest-priced member.
  3. Otherwise, the first member in the cart.
The price fallback is usually right, since bundle apps tend to put the discount on the main product. Set the role explicitly when it isn’t, for example when the main product is the cheapest item or is free.

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 setHidden never becomes part of a bundle, and a comparator sees the anchor, not the children.
Grouped children leave state.items. Once lines are folded into a bundle, only the anchor appears in getCart().items and in event payloads; the children move to anchor.bundle.children. They also stop counting toward itemCount.The cart total is unaffected, because totals come straight from Shopify. Grouping changes presentation, never what the shopper pays.

Reading a bundle back

To act on a bundle’s lines, use bundle.memberKeys, which holds the key of every member including the anchor.

Using it for other things

Bundle grouping is what setInternalProperties 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:
Use it when the value is derived and display-only. If the data needs to survive to the order, it belongs in a real line item property, set with a hidden properties[...] input on the product form so it reaches Shopify whoever performs the add.

Where to go next