Cart line without selling plan gets selling-plan price in Function input when same variant has a subscription line in cart

We are facing a problem with Shopify Functions input when selling-plan (subscription) lines and normal lines of the same variant are in the cart together.

Our setup

We run two completely separate flows and keep them separate using different line item properties:

Flow Line item property Handled by
One-time bundles _easyBundle:OfferId Cart Transform (cart.transform.run) — merges the lines
Subscription bundles _bundleOfferId + selling plan Discount function (cart.lines.discounts.generate.run)

Cart Transform never touches subscription lines (it doesn’t support selling-plan lines) — that’s exactly why we split the flows. So the two flows never share a line. But the prices still leak from one to the other.

Example

A wallet costs Rs. 200. It has a subscription plan “60% off” (plan price Rs. 80).

  1. Customer adds the wallet in a one-time bundle → cart line X, no selling plan. Our Cart Transform input shows line X = Rs. 200. Correct.
  2. Customer then adds the same wallet in a subscription bundle → new cart line Y, with the selling plan, Rs. 80. Fine.
  3. Our Cart Transform runs again. Line X is untouched — same line, same ID, still no selling plan. But its price in the input is now Rs. 80.
cart.transform.run input, line X (CartLine no selling plan):

  run 1 (only one-time lines in cart):     subtotalAmount = 200.0  ✓
  run 2 (subscription lines also in cart): subtotalAmount =  80.0  ✗

Line X just inherited line Y’s selling-plan price because they’re the same variant. Our function’s math runs on these amounts, so every price it computes for the merged bundle fluctuates depending on whether a subscription line of the same variant happens to be in the cart. Our property-based filtering can’t protect us, because the leak is in cost.subtotalAmount itself, not in which lines we read.

Questions

  1. Is this expected? Should a no-plan cart line’s cost change because a selling-plan line of the same variant exists in the cart?
  2. If it is expected — how can a function read the line’s real (pre-selling-plan) price? ProductVariant has no price field and SellingPlanAllocationPriceAdjustment has no compareAtPrice in the Functions input schema (both exist in the Storefront API). Can these be exposed?

One cart line, before vs after

Same product, same one-time bundle line (_easyBundle:OfferId present, no selling plan). The only difference between the two runs: subscription lines of the same variant were added to the cart.

Run 1 — no subscription lines in cart → correct price (Rs. 200):

{
  "id": "gid://shopify/CartLine/f33e9eed-38fa-4786-b142-9c1c0f7c415f",
  "lineOfferId": { "key": "_easyBundle:OfferId", "value": "PB-133044_VX0_1" },
  "quantity": 1,
  "cost": {
    "subtotalAmount": { "amount": "200.0", "currencyCode": "INR" }
  },
  "merchandise": {
    "__typename": "ProductVariant",
    "id": "gid://shopify/ProductVariant/46105732219131",
    "title": "Black",
    "product": { "title": "Leather Zip Wallet Updated" }
  }
}

Run 2 — subscription lines of the same variant now in cart → price arrives already reduced (Rs. 80 = 60% selling-plan price):

{
  "id": "gid://shopify/CartLine/2a960a74-d948-43c9-a590-cfa047f7ec7c",
  "lineOfferId": { "key": "_easyBundle:OfferId", "value": "PB-133044_WZ9_1" },
  "quantity": 1,
  "cost": {
    "subtotalAmount": { "amount": "80.0", "currencyCode": "INR" }
  },
  "merchandise": {
    "__typename": "ProductVariant",
    "id": "gid://shopify/ProductVariant/46105732219131",
    "title": "Black",
    "product": { "title": "Leather Zip Wallet Updated" }
  }
}

Same variant (ProductVariant/46105732219131), same kind of one-time line, no selling plan on it — yet cost.subtotalAmount dropped from 200.0 → 80.0 purely because a subscription line of that variant exists in the cart. Our Cart Transform math runs on this amount, so the merged bundle price fluctuates.

Hi Asif,

Based on the behaviour you documented, this does not look like expected line-level pricing. A cart line without a sellingPlanAllocation should not inherit the subscription price simply because another line uses the same variant with a selling plan.

Shopify defines CartLine.cost.subtotalAmount as the cost of that specific cart line before line-level discounts, while sellingPlanAllocation belongs to the individual line. Your two runs suggest Shopify is resolving the variant’s selling-plan price at variant level and leaking it into the unrelated one-time line before the Cart Transform receives its input.

A few practical points:

  1. Do not use the affected subtotalAmount as the source of truth for bundle pricing while this behaviour is present.

  2. Add these fields to the Function input if available in your API version:

    • cost.amountPerQuantity
    • cost.compareAtAmountPerQuantity
    • sellingPlanAllocation

    compareAtAmountPerQuantity exists on CartLineCost in the current Functions schema and is described as the per-unit cost before discounts. Test whether it remains at Rs. 200 when the leak occurs.

  3. If that field also becomes Rs. 80, there is no reliable base catalogue price available inside the current Function input. The Storefront API exposes ProductVariant.price and compareAtPrice, but those fields are not generally available on the Function’s ProductVariant object.

  4. As a temporary workaround, store the base variant price in an app-owned variant metafield and query that metafield inside the Cart Transform. Then calculate the one-time bundle from that value rather than from line.cost.

  5. Create a minimal reproduction with:

    • One variant
    • One percentage selling plan
    • One normal line
    • One subscription line
    • No discount function
    • A Cart Transform that only logs the input

    If the normal line still changes from Rs. 200 to Rs. 80, submit it to Shopify as a Functions pricing bug with both full input payloads, API version, cart creation method and selling-plan configuration.

I would also test on the latest Functions API version because the schema now exposes additional line-cost fields, but I would not redesign your property-based separation, the filtering logic is sound. The incorrect value is already present before your Function processes the line.

Best of luck

One detail in the repro is worth tightening before treating this as the same cart line changing price.

The two JSON samples have different CartLine IDs and different _easyBundle:OfferId values. They show the same variant arriving at Rs. 200 and Rs. 80 in two inputs, but they do not yet prove that the same logical one-time line changed price.

I would capture the complete Cart Transform input immediately before and after adding the subscription line, and include this for every line:

sellingPlanAllocation {
sellingPlan {
id
}
}

The current snippets do not request sellingPlanAllocation, so its absence from the JSON does not confirm that the Rs. 80 line has no selling plan attached.

There is also a separate Cart Transform limitation to verify. Shopify’s current docs say lineExpand, linesMerge, and lineUpdate operations are rejected if a selling plan is present. Since this cart contains a subscription line, check the Function result and logs to confirm whether your linesMerge output is actually being accepted and applied.

One caution on compareAtAmountPerQuantity: it is the compare-at price, not a guaranteed original one-time price, and it can be null or buyer-dependent. A variant metafield can still be an app-controlled fallback, but it needs synchronization and presentment-currency handling.

If the complete before-and-after inputs show the same logical one-time line, sellingPlanAllocation: null, and its cost changing from Rs. 200 to Rs. 80, that would make a much stronger Functions bug report.

Thanks for the pointers — we re-captured with sellingPlanAllocation { sellingPlan { id } } and cost.amountPerQuantity added to the input query for every line. The results make the case tighter:

Same product example, re-captured

“Leather Zip Wallet Updated” (ProductVariant/46105732219131), one-time price Rs. 200, selling plan 60% off (Rs. 80).

Capture 1 — only the one-time bundle in the cart. The one-time line reports no selling plan and the correct price:

{
  "id": "gid://shopify/CartLine/22d5a227-2513-4e91-af67-cb167b62bb11",
  "lineOfferId": { "key": "_easyBundle:OfferId", "value": "PB-133044_FNS_1" },
  "sellingPlanAllocation": null,
  "quantity": 1,
  "cost": {
    "amountPerQuantity": { "amount": "200.0", "currencyCode": "INR" },
    "subtotalAmount":    { "amount": "200.0", "currencyCode": "INR" }
  },
  "merchandise": { "id": "gid://shopify/ProductVariant/46105732219131" }
}

Capture 2 — subscription lines of the same variant now also in the cart. The one-time line still reports sellingPlanAllocation: null, yet BOTH cost fields arrive at the plan price:

{
  "id": "gid://shopify/CartLine/bb93c3aa-b37e-4d8b-be52-0cde5b70c406",
  "lineOfferId": { "key": "_easyBundle:OfferId", "value": "PB-133044_IH0_1" },
  "sellingPlanAllocation": null,
  "quantity": 1,
  "cost": {
    "amountPerQuantity": { "amount": "80.0", "currencyCode": "INR" },
    "subtotalAmount":    { "amount": "80.0", "currencyCode": "INR" }
  },
  "merchandise": { "id": "gid://shopify/ProductVariant/46105732219131" }
}

So amountPerQuantity is not a way out — it is reduced exactly like subtotalAmount, on a line whose sellingPlanAllocation is null.

On your specific points

  1. “Different CartLine IDs don’t prove the same line changed price.” Fair — and our earlier amount-off captures already contain that case: the SAME CartLine gid 3c64bc10-d662-4081-a0f5-3295a14d39a6 (one-time wallet line, _easyBundle:OfferId present) appears with subtotalAmount 200.0 in the run before the subscription lines were added and 80.0 in the run after. The line was never modified; only selling-plan lines of the same variant were added. Combined with the new captures showing sellingPlanAllocation: null on that class of line, this is the same logical one-time line changing price.

  2. “Merge operations are rejected when a selling plan is present.” Verified: our linesMerge only ever targets the no-plan lines (the subscription lines carry a different property and are never included in cartLines). The operation is accepted and applied — the merged parent renders in cart and checkout. The docs limitation applies to merging lines that have plans; we don’t.

  3. compareAtAmountPerQuantity / metafield fallback — agreed on the caveats; that’s exactly why we’d prefer a reliable primitive. Which brings back the core questions:

    • Why does a line with sellingPlanAllocation: null get selling-plan pricing in cost.amountPerQuantity / cost.subtotalAmount when another line of the same variant has a plan?

Steps to reproduce

Setup

Our app sells the same bundle in two purchase modes, handled by two different Shopify Functions:

Purchase mode How lines are added Line item property Handled by
One-time without a selling plan _easyBundle:OfferId Cart Transform (cart.transform.run) — merges the lines into one parent
Subscription with a selling plan _bundleOfferId Discount function (cart.lines.discounts.generate.run) — applies the bundle discount

Cart Transform never touches the subscription lines (merge is not supported on selling-plan lines) — that’s exactly why the flows are split.

Test data: “Leather Zip Wallet Updated” (ProductVariant/46105732219131) — one-time price Rs. 200, selling plan “60% off” (plan price Rs. 80).

Cart Transform input query includes per line:

cost { amountPerQuantity { amount } subtotalAmount { amount } }
sellingPlanAllocation { sellingPlan { id } }
lineOfferId: attribute(key: "_easyBundle:OfferId") { value }

Repro

  1. Clear the cart.
  2. Add the bundle as a one-time purchase → wallet line lands with _easyBundle:OfferId, no selling_plan.
  3. Capture the Cart Transform input → the wallet line shows amountPerQuantity = 200.0, subtotalAmount = 200.0, sellingPlanAllocation: null. The linesMerge output prices the bundle correctly. :white_check_mark:
  4. Add the same bundle as a subscription → a second wallet line lands with selling_plan (+ _bundleOfferId). The Discount function applies the subscription discount to it. :white_check_mark:
  5. Cart Transform runs again — capture its input and inspect the one-time wallet line from step 2/3.

Expected

The one-time wallet line is unchanged:

amountPerQuantity = 200.0 · subtotalAmount = 200.0 · sellingPlanAllocation: null

Only the subscription line carries the plan price.

Actual

  • The one-time wallet line — still tagged _easyBundle:OfferId, still sellingPlanAllocation: null — now arrives at:
amountPerQuantity = 80.0 · subtotalAmount = 80.0   (the selling-plan price)

Our Cart Transform merge math now runs on 80 instead of 200, so the merged bundle price is wrong.

  • The subscription wallet line also reports sellingPlanAllocation: null in the Cart Transform input, so the two lines cannot be told apart inside the function — while /cart.js for the same cart correctly shows the selling plan on only that line.

@Asif_Malik
This looks more like a platform limitation or possibly a bug than an issue with your implementation.

In my understanding each cart line should remain independent. A one-time purchase line without a selling plan shouldnt inherit the subscription price simply because another line with the same variant uses a selling plan. Since cart.transform.run operates on individual cart lines I’d expect cost.subtotalAmount to reflect that specific line’s pricing, not another lines selling plan allocation.

The bigger issue is that Shopify Functions doesnt currently expose the variant’s base price (or compare-at/base selling plan data) in the input so theres no reliable way to reconstruct the original price when cost.subtotalAmount has already been adjusted.

I’d recommend opening a Partner Support ticket with the exact payloads you have shared. If this behavior is intentional Shopify should expose a non-discounted price field in the Functions schema. If its unintentional it seems like a genuine bug where the selling plan price is leaking across cart lines that share the same variant ID.

This definitely doesn’t look like the expected behavior.

Based on your examples, the one-time cart line has no selling plan, a different CartLine ID, and your Cart Transform only processes lines with _easyBundle:OfferId. Despite that, cost.subtotalAmount changes from Rs. 200 to Rs. 80 simply because another cart line of the same ProductVariant has a selling plan.

If that’s how the Functions API is intended to behave, it creates a significant problem for developers. cart.transform.run can no longer rely on cost.subtotalAmount to represent the actual price of the line it’s processing, since the value appears to be influenced by another line in the cart.

I’d also like clarification on two points:

  • Is this a bug, or is cost.subtotalAmount intentionally resolved at the variant level when selling plans are present?
  • If this is expected behavior, can Shopify expose the variant’s base price (or pre-selling-plan price) in the Functions input? Without access to that value, it’s very difficult to build reliable pricing logic for carts that contain both subscription and one-time purchases of the same variant.

This seems like an important edge case for anyone building Shopify Functions that support both subscriptions and one-time purchases.

Thanks, this closes the two gaps I was concerned about.

Before-and-after capture with the same CartLine ID, plus sellingPlanAllocation remaining null while both cost fields change from 200 to 80, is the key evidence. Confirming that linesMerge is accepted also rules out the operation being silently rejected.

Subscription line returning sellingPlanAllocation: null in the Cart Transform input while /cart.js shows the plan makes this look like a platform-side input issue or undocumented limitation, not a problem with your property filtering. I think this is ready for Partner Support with the minimal repro, full Function inputs and outputs, API version, and run timestamps. I do not see a reliable in-Function workaround based on line.cost.

#9s conclusion is the right one and i’d push it one step further: theres no fix here that starts from line.cost, so stop reading it. the money for a bundle shouldnt come out of the cart in the first place.

you already stamp _easyBundle:OfferId on every line. that key is enough — the function can look the offers pricing up from your own app-owned data instead of deriving it from cart money. app-owned metafields and metaobjects are readable straight from the function input query, and unlike line.cost nothing else in the cart can rewrite them. the variant level leak then stops being something you work around and just becomes irrelevant to your math. thats also strictly better than the variant metafield in #3, because it keys on the offer rather than the variant, and the offer is the thing that actually owns the price — two lines of the same variant sitting in two different offers each resolve correctly, which is exactly the case thats breaking you.

the lighter version, if you dont want a lookup on every run, is to carry the number itself: stamp the total in integer cents as a line property at add time, when the storefront still knows the correct one-time price because it hasnt met the subscription line yet, and read it with attribute(key:) like you already read the offer id. it also freezes at add time, which for a bundle offer is usually what you want, and properties are part of line identity in shopify so it cant drift under you — change one and you get a different line, not a mutated one.

but if you go that way, treat it as a claim and not a fact. anyone can post arbitrary properties to /cart/add, so a price that arrives on the line is shopper supplied by definition. the function has to validate it against something you own and refuse when it doesnt line up. we fail closed on exactly one of those checks: if the stamped currency doesnt match the carts presentment currency, our functions apply no discount and no merge at all, rather than something plausible looking. and thats the same hole cuongnm flagged on the metafield route — a cents integer captured in the browser is in shop currency while the function sees presentment money, so we only stamp it when the rate is 1 and drop the mechanism otherwise instead of shipping a confidently wrong total. currency doesnt get solved by either route, it just becomes detectable.

for what its worth your split is the part i’d keep. we do the same, and refuse the merge outright the moment a selling plan is anywhere in the group.

disclosure, i build a bundle app (Verve), so this is the same corner i live in.