Hi Shopify team,
We are seeing a cart-sync issue on POS with bundles produced by a Cart Transform merge, reproducible with native POS adds alone. Details below.
On Shopify POS 11.14.0, when the cart contains a bundle produced by a Cart Transform linesMerge operation and the cart then acquires a separate loose line for every component variant of that bundle, POS’s cart sync removes the merged bundle from the server-side cart and adds the bundle’s component units onto the loose lines. The device keeps showing the bundle and the loose lines at their original quantities, so the device cart and the cart the Function receives disagree from that moment on.
The trigger is completing the set. One or two loose components leave the bundle intact. The third (last) one dissolves it. It happens with native POS product-search adds, so it is not caused by the UI Extensions Cart API, by an app’s Function, or by line item properties.
For merchants this shows up as: a bundle vanishing from the cart when its products are also rung up individually, a second bundle of the same kind producing an extra bundle and an overcharge, and Cart Transform merges failing with leftover lines when a second bundle contains every product of the first. A second bundle that leaves out even one of the first bundle’s products is unaffected.
Environment
- Shopify POS app 11.14.0 (
session.currentSession.posVersion) - Last known working version: none — this is the first POS version we have tested merge bundles on, so we cannot say whether it is a regression
- POS UI Extensions API
2026-07,@shopify/ui-extensions2026.7.1 - Cart Transform Function API
2026-01, Rust/WASM, targetcart.transform.run, singlelinesMergeper bundle - Bundle parent variant has
requiresComponents: true(tested both ways, no difference) - Cart API usage by our extension: only
shopify.cart.addLineItem(variantId, quantity, { properties })— one call per component line, properties attached in the same call — andshopify.cart.removeLineItem(uuid)for rollback. Cart state is read fromshopify.cart.current.value/subscribe. We do not usebulkCartUpdate,addLineItemProperties,bulkAddLineItemProperties, or any discount method. (One earlier experiment withbulkCartUpdateas a repair step was reverted; it never fired because the device cart already looked correct.) - Development store, single device
- Dates: 2026-09-16 and 2026-09-17 (UTC timestamps below)
Minimal reproduction — native POS only (2026-09-17)
Steps
-
Empty cart. Get a Cart Transform merged bundle into the cart whose components are three product variants A, B, C (one unit each). Ours arrives via a UI extension that makes three calls of the form below, after which the Function returns one
linesMergeinto parent variant P:await shopify.cart.addLineItem(variantId, 1, { properties: { "_easyBundle:OfferId": "PB-269266_8WB_1", "_easyBundle:prodQty": "1", _bundleName: "product bundle", Box: "1" } });From here on the extension is not used.
-
In POS’s own product search, add A, quantity 1.
-
Add C, quantity 1.
-
Add B, quantity 1.
Expected: the merged bundle stays in the cart; three loose lines A(1) B(1) C(1) sit beside it.
Actual: after step 4 the bundle is gone from the server-side cart and the loose lines are A(2) B(2) C(2). The device still shows the bundle and the three loose lines at 1.
| UTC | Action | Cart the Function received |
|---|---|---|
| 07:53:36 | bundle added, merged | A(1) B(1) C(1) tagged → linesMerge into P |
| 07:53:42 | POS product search: add A, qty 1 | bundle intact + loose A(1) |
| 07:54:02 | POS product search: add C, qty 1 | bundle intact + loose A(1) C(1) |
| 07:54:03 | POS product search: add B, qty 1 | tagged lines gone. loose A(2) B(2) C(2). Function emits no operations. |
The device cart still showed the bundle line (with 3 components) plus three loose lines at quantity 1.
Elimination matrix
Every probe below was run against a freshly merged bundle in an otherwise empty cart, on the same device, same day. “Clean” = bundle intact, new lines landed at the requested quantity.
| Probe | Result |
|---|---|
Cart API addLineItem, ONE component, qty 1, one property |
clean |
Cart API addLineItem, ONE component, qty 1, carrying the bundle tag (Function merges it into a second one-line bundle of the same parent variant) |
clean |
Cart API, all THREE components, three concurrent addLineItem calls, untagged |
bundle dissolved, all three lines at 2 |
| Cart API, all THREE components, sequential, 1.5 s apart, untagged | first two clean; third dissolves the bundle, all three lines at 2 |
| Native product search, ONE component | clean |
| Native product search, all THREE components one at a time | first two clean; third dissolves the bundle, all three lines at 2 |
| Same bundle written twice by our web storefront (Ajax cart), identical lines and tags | clean — second set lands at 1 |
| Second bundle whose components do not overlap the first | clean |
| Function forced to emit no operations (no merged parent exists), same adds | clean |
Ruled out by the matrix: concurrency, the UI Extensions Cart API path, line item properties / bundle tags, our Function’s output, requiresComponents. The one necessary condition is a merged bundle plus loose lines covering all of its components.
How this surfaces through a bundle app
Our extension writes a bundle as N tagged component lines that the Function merges. Consequences on POS, all from the raw Function input:
- Same bundle twice (19:58:49 → 19:58:59, 2026-09-16): the second set of lines completes the first bundle’s component set, so the second set arrives at quantity 2 on every line.
linesMergethen yields a parent at quantity 2 → three bundles in the cart and an overcharge, no error. - Repeated adds double every time (15:44–15:47, 2026-09-16): the same bundle added four times landed each component at 1 → 2 → 4 → 8 (2 → 4 → 8 → 16 for the one product the cashier picked twice).
- Second bundle that contains all of the first bundle’s products (07:35:40, 2026-09-17): the first bundle is removed from the server cart; the second bundle’s shared lines arrive at 2, 3, 2 and its non-shared lines at 1. The merge is left with excess units; Shopify splits them into extra tagged lines and merges those into an oversized bundle at a wrong price (3074.90 observed on the device, against a quoted 2262.40). The cashier sees the first bundle disappear and a wrong bundle appear.
Exact arithmetic on a larger cart (19:13:43, 2026-09-16): with 15 units of each of A/B/C inside four merged bundles and 2 units of each of D/E/F inside two others, adding one unit of each (two of A) produced A at 17, B/C at 16 and D/E/F at 3 — landed = units already inside merged bundles + requested, on every line.
Impact
- A merchant is overcharged or undercharged without any error: two identical bundles become three, a bundle disappears when its products are also sold individually in the same sale.
- The same bundle cannot be sold twice in one POS cart, and a bundle cannot be followed by a second bundle that contains all of its products — the second add fails or corrupts the first. Two bundles that differ by at least one product are fine.
- The extension cannot detect or repair it, because the device-side cart it can read never shows the change. Only the Function input (and the resulting order) does.
- This affects any app that represents bundles with
linesMerge, not only ours.
Why we believe this is POS cart sync
- The Function input is Shopify’s record of the cart before the Function runs; the change happens before that point.
- The device’s cart (
shopify.cart.current.value) never reflects the change. - The web storefront, where Shopify’s server owns the cart, does not show it with identical lines.
- It reproduces with native adds where no app code runs.
The simplest explanation consistent with every run is that, when reconciling the local cart, POS treats loose lines that match all of a merged bundle’s component variants as that bundle’s source lines and folds the bundle into them. We cannot see the sync payload, so we are asking Shopify to confirm.
Questions
- Is there a supported way to keep a
linesMergebundle intact on POS when its products are also in the cart as loose lines or inside a second bundle? For example, a way to write component lines from a POS UI extension that the cart sync will not fold. - If
linesMergeis not the intended model on POS, islineExpand(one parent line per bundle) the recommended one? In our tests expand does not trigger this. It does run into the one-line-per-variant behaviour when two bundles share a parent variant, so two different configurations of the same bundle cannot coexist. Any guidance on that would help. - If a fix is planned, which POS version will carry it?
Thanks for taking a look.
Function input, verbatim (the two decisive runs)
Straight from the CLI’s function run logs (shopify app dev), cart.lines and operations only; line ids shortened to their last 8 characters. Steps 2 and 3 have already been done at this point: the bundle is merged and two of its three components sit in the cart as loose lines.
07:54:02Z — after native add of the second loose component (C). Bundle intact, loose lines at 1.
lines
8ce9535c qty 1 variant 42604105728087 _easyBundle:OfferId=PB-269266_8WB_1 prodQty=1
fba6cbb9 qty 1 variant 42604106612823 _easyBundle:OfferId=PB-269266_8WB_2 prodQty=1
b13ebc7c qty 1 variant 42604106448983 _easyBundle:OfferId=PB-269266_8WB_3 prodQty=1
509753dc qty 1 variant 42604105728087 (loose, no attributes)
6fef1f23 qty 1 variant 42604106448983 (loose, no attributes)
operations
linesMerge parentVariantId=44303071608919 cartLines=[8ce9535c:1, fba6cbb9:1, b13ebc7c:1]
07:54:03Z — one second later, after native add of the third loose component (B). Tagged lines gone, every loose line at 2.
lines
509753dc qty 2 variant 42604105728087 (loose, no attributes)
6fef1f23 qty 2 variant 42604106448983 (loose, no attributes)
a0a5b11b qty 2 variant 42604106612823 (loose, no attributes)
operations
(none — no tagged lines left to merge)
The device cart at the same moment (read through shopify.cart.current.value) showed the merged bundle line with 3 components plus three loose lines at quantity 1.