cart.lines.discounts.generate.run - [extensions.input.variables] binding always returns null. What's the actual working syntax?

I’m building a discount Function on cart.lines.discounts.generate.run (API 2026-04, public app). I want product.inCollections(ids: $rulesProductCollections) where $rulesProductCollections is populated at runtime from a shop metafield the merchant configures through my app. Every configuration I’ve tried either fails CLI deploy or makes the binding silently no-op. Looking for someone who has this working in production and can tell me the actual right syntax.

WHAT I’VE TRIED

Form A (deploys, binding always returns null):

[[extensions]]
type = "function"

  [[extensions.targeting]]
  target = "cart.lines.discounts.generate.run"
  input_query = "src/run.graphql"
  export = "run"

  [extensions.input.variables]
  namespace = "$app:custom_discounts"
  key       = "rules_product_collections"

GraphQL:

query RunInput($rulesProductCollections: [ID!]!) {
  cart { lines { merchandise { ... on ProductVariant { product {
    inCollections(ids: $rulesProductCollections) { collectionId isMember }
  }}}}}
}

Shop metafield (type json, namespace $app:custom_discounts, key rules_product_collections). I tried two value shapes:

  • Bare array: [“gid://shopify/Collection/359951696026”]
  • Object with matching key: {“rulesProductCollections”:[“gid://shopify/Collection/359951696026”]}

Both deploy fine. Both produce the same runtime error on every checkout:

Variable $rulesProductCollections of type [ID!]! was provided invalid value
extensions: { code: INVALID_VARIABLE, value: null,
              problems: [{ path: [], explanation: "Expected value to not be null" }] }

The metafield genuinely exists with whichever value I set — I read it directly in the same input_query via shop.metafield(namespace, key) { value } and see the JSON intact at runtime. The binding is just always returning null.

Form B (rejected by CLI):

[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/run.graphql"
export = "run"

  [extensions.targeting.input.variables]
  namespace = "$app:custom_discounts"
  key       = "rules_product_collections"

npx shopify app deploy rejects this with:

Validation errors
- input.variables: Variable definitions are missing. Ensure you have defined them
  in your extension configuration and that you are using a supported CLI version.
  See documentation for details: https://shopify.dev/api/functions/input-query-variables

I also tried the subtable form [extensions.targeting.input.variables.rulesProductCollections] — same rejection.

The docs URL the error points at returns a vague page that doesn’t show a complete working example for cart.lines.discounts.generate.run. Shopify’s AI assistant gave me three “fixes” that turned out either deploy-breaking or runtime-breaking.

WHAT I’D LOVE TO KNOW FROM SOMEONE WHO’S DONE THIS

  1. Which @shopify/cli version do I need for Form B (per-target placement)
    to actually deploy on this target?
  2. With Form A (extension-scope placement) — is the binding meant to work
    at all, and if so, what shape does the metafield value have to be?
  3. Does the GraphQL variable name need to match the metafield key
    (snake_case → camelCase auto-conversion, or literal match)?
  4. If [extensions.input.variables] truly doesn’t work for a shop metafield
    on cart.lines.discounts.generate.run, what’s the canonical Shopify
    pattern for dynamic collection-based product discounts on this target?
    I’ll fall back to expanding collections to product IDs at save time
    plus a collections/update webhook — but I’d rather use input-variable
    binding if it’s supposed to work.

If you have a working shopify.extension.toml + run.graphql + metafield value triple for this target, please paste it. CLI version too if you remember it. That single example would save the next person hitting this several days of guessing.

Thanks.

Hey @domenet - thanks for the detailed writeup, that really helped define the issue clearly.

I did some digging and reproduced the error you were seeing, which helped me diagnose then diagnose the suspected cause: The input variable binds the metafield from the function’s owner, which is the discount itself (the DiscountAutomaticNode or DiscountCodeNode), not the shop. So if the value is written on the shop, the binding comes back null even though a shop { metafield(...) } read in the same query returns it fine. That mismatch is almost always what’s behind the INVALID_VARIABLE / value: null you’re seeing, and it’s why the value looks “present” but never reaches the variable.

I set this up on my end to confirm. With the value on the shop I got the same error you saw in “Form A”. Moving the same value onto the discount owner, nothing else changed, made the variable resolve.

The working setup looks like this. The toml block sits at extension scope (a sibling of [[extensions.targeting]], not nested inside it):

[extensions.input.variables]
namespace = "custom_discounts"
key = "rules_product_collections"

Here’s the input query, with the variable typed as you’d expect:

query RunInput($rulesProductCollections: [ID!]!) {
  cart {
    lines {
      merchandise {
        ... on ProductVariant {
          product {
            inCollections(ids: $rulesProductCollections) {
              isMember
            }
          }
        }
      }
    }
  }
}

Then write the metafield on the discount, with the JSON key matching the variable name:

mutation {
  metafieldsSet(metafields: [{
    ownerId: "gid://shopify/DiscountAutomaticNode/123",
    namespace: "custom_discounts",
    key: "rules_product_collections",
    type: "json",
    value: "{\"rulesProductCollections\":[\"gid://shopify/Collection/456\"]}"
  }]) { metafields { id } userErrors { field message } }
}

There’s more information on the owner detail here: Use variables in input queries

Let me know if I can clarify anything further and I hope this helps!

Thank you — this was exactly the missing piece for us.

Confirming for anyone who finds this thread later: [extensions.input.variables] in shopify.extension.toml does work on cart.lines.discounts.generate.run. The gotcha that had us stuck was the owner: the metafield has to live on the DiscountAutomaticNode, not on the shop.

Once we set the metafield (same namespace / key declared in the toml) on gid://shopify/DiscountAutomaticNode/<id>, the input variable resolved and product.inCollections(ids: $rulesProductCollections) returned proper membership results inside run.graphql — verified end-to-end in the Function run log.

Really appreciate the help — marking your answer as the solution.