# Free gift with purchase

> A Shopify discount Function that makes one gift item free once the rest of the cart reaches a threshold. The gift must already be in the cart.

Page: https://lintelagency.com/resources/shopify-functions/free-gift-with-purchase

## In short

This discount Function makes one unit of a gift product free once the rest of the cart reaches a threshold, 75 in your store currency by default. It can’t add the gift: a discount Function only discounts lines that are already in the cart, so the theme, Storefront API or a cart transform has to add it. It uses cart.lines.discounts.generate.run, Discount Function API 2026-10.

## Facts

- **Function API:** Discount
- **Target:** cart.lines.discounts.generate.run
- **Discount class:** PRODUCT
- **Plan:** Any plan in a public app; Shopify Plus in a custom app
- **App scope:** write_discounts
- **API version:** 2026-10
- **Last checked:** 2 November 2026

## How to use it

1. Create the gift as a product variant and keep it out of your collections.
2. Create an app with the Shopify CLI, add the extension (shopify app generate extension --template discount) and replace its files with the files below.
3. Run the fixtures with shopify app function run --input tests/threshold-met.input.json, then deploy.
4. Create an automatic discount with discountAutomaticAppCreate, discountClasses: [PRODUCT], and set {"giftVariantId": "gid://shopify/ProductVariant/1234567890", "minimumAmount": 75} in its metafield.
5. Add the gift to the cart from the theme (for example with /cart/add.js when the threshold is reached) and remove it when the cart drops below.

## Code

### shopify.extension.toml

```toml
api_version = "2026-10"

[[extensions]]
name = "Free gift with purchase"
handle = "free-gift-with-purchase"
type = "function"

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

  [extensions.build]
  command = ""
  path = "dist/function.wasm"
```

### src/cart_lines_discounts_generate_run.graphql

```graphql
query CartLinesDiscountsGenerateRunInput {
  cart {
    lines {
      id
      cost {
        subtotalAmount {
          amount
        }
      }
      merchandise {
        __typename
        ... on ProductVariant {
          id
        }
      }
    }
  }
  presentmentCurrencyRate
  discount {
    discountClasses
    metafield(namespace: "$app", key: "function-configuration") {
      jsonValue
    }
  }
}
```

### src/cart_lines_discounts_generate_run.js

```js
// @ts-check

/**
 * @typedef {import("../generated/api").CartLinesDiscountsGenerateRunInput} CartLinesDiscountsGenerateRunInput
 * @typedef {import("../generated/api").CartLinesDiscountsGenerateRunResult} CartLinesDiscountsGenerateRunResult
 */

/**
 * Settings stored as JSON in the discount's metafield. A discount Function
 * can't add lines to the cart: the gift has to be added by the theme, the
 * Storefront API or a cart transform. This Function only makes it free.
 */
const DEFAULT_CONFIG = {
  giftVariantId: /** @type {string | null} */ (null),
  // Spend on other products, in the store's currency, that unlocks the gift.
  minimumAmount: 75,
  message: "Free gift",
};

/** @type {CartLinesDiscountsGenerateRunResult} */
const NO_DISCOUNT = { operations: [] };

/**
 * @param {CartLinesDiscountsGenerateRunInput} input
 * @returns {CartLinesDiscountsGenerateRunResult}
 */
export function cartLinesDiscountsGenerateRun(input) {
  // A Function may only return operations for the classes the discount was created with.
  if (!input.discount.discountClasses.includes("PRODUCT")) return NO_DISCOUNT;
  const config = { ...DEFAULT_CONFIG, ...input.discount.metafield?.jsonValue };
  if (!config.giftVariantId) return NO_DISCOUNT;

  /** @param {(typeof input.cart.lines)[number]} line */
  const isGift = (line) => line.merchandise.__typename === "ProductVariant" && line.merchandise.id === config.giftVariantId;
  const giftLine = input.cart.lines.find(isGift);
  if (!giftLine) return NO_DISCOUNT;

  // The gift itself doesn't count towards the threshold.
  const spend = input.cart.lines
    .filter((line) => !isGift(line))
    .reduce((sum, line) => sum + Number(line.cost.subtotalAmount.amount), 0);
  if (spend < config.minimumAmount * Number(input.presentmentCurrencyRate)) return NO_DISCOUNT;

  // Only one unit is free; extra units of the gift are charged normally.
  return {
    operations: [
      {
        productDiscountsAdd: {
          candidates: [
            {
              message: config.message,
              targets: [{ cartLine: { id: giftLine.id, quantity: 1 } }],
              value: { percentage: { value: "100" } },
            },
          ],
          selectionStrategy: "FIRST",
        },
      },
    ],
  };
}
```

## Test input and expected output

### tests/below-threshold.input.json

```json
{
  "cart": {
    "lines": [
      { "id": "gid://shopify/CartLine/1", "cost": { "subtotalAmount": { "amount": "60.0" } }, "merchandise": { "__typename": "ProductVariant", "id": "gid://shopify/ProductVariant/1111111111" } },
      { "id": "gid://shopify/CartLine/2", "cost": { "subtotalAmount": { "amount": "15.0" } }, "merchandise": { "__typename": "ProductVariant", "id": "gid://shopify/ProductVariant/1234567890" } }
    ]
  },
  "presentmentCurrencyRate": "1.0",
  "discount": {
    "discountClasses": ["PRODUCT"],
    "metafield": { "jsonValue": { "giftVariantId": "gid://shopify/ProductVariant/1234567890", "minimumAmount": 75, "message": "Free tote bag" } }
  }
}
```

### tests/below-threshold.output.json

```json
{ "operations": [] }
```

### tests/gift-not-in-cart.input.json

```json
{
  "cart": {
    "lines": [
      { "id": "gid://shopify/CartLine/1", "cost": { "subtotalAmount": { "amount": "120.0" } }, "merchandise": { "__typename": "ProductVariant", "id": "gid://shopify/ProductVariant/1111111111" } }
    ]
  },
  "presentmentCurrencyRate": "1.0",
  "discount": {
    "discountClasses": ["PRODUCT"],
    "metafield": { "jsonValue": { "giftVariantId": "gid://shopify/ProductVariant/1234567890", "minimumAmount": 75, "message": "Free tote bag" } }
  }
}
```

### tests/gift-not-in-cart.output.json

```json
{ "operations": [] }
```

### tests/threshold-met.input.json

```json
{
  "cart": {
    "lines": [
      { "id": "gid://shopify/CartLine/1", "cost": { "subtotalAmount": { "amount": "80.0" } }, "merchandise": { "__typename": "ProductVariant", "id": "gid://shopify/ProductVariant/1111111111" } },
      { "id": "gid://shopify/CartLine/2", "cost": { "subtotalAmount": { "amount": "15.0" } }, "merchandise": { "__typename": "ProductVariant", "id": "gid://shopify/ProductVariant/1234567890" } }
    ]
  },
  "presentmentCurrencyRate": "1.0",
  "discount": {
    "discountClasses": ["PRODUCT"],
    "metafield": { "jsonValue": { "giftVariantId": "gid://shopify/ProductVariant/1234567890", "minimumAmount": 75, "message": "Free tote bag" } }
  }
}
```

### tests/threshold-met.output.json

```json
{
  "operations": [
    {
      "productDiscountsAdd": {
        "candidates": [
          {
            "message": "Free tote bag",
            "targets": [{ "cartLine": { "id": "gid://shopify/CartLine/2", "quantity": 1 } }],
            "value": { "percentage": { "value": "100" } }
          }
        ],
        "selectionStrategy": "FIRST"
      }
    }
  ]
}
```

## Limits and edge cases

- Discount Functions can’t create cart lines. Without a theme or app step that adds the gift, nothing happens.
- Only one unit is free; more units of the gift are charged at the normal price.
- The gift itself doesn’t count towards the threshold, so it can’t qualify itself.

## Questions

### Can a Shopify Function add a free product to the cart?

Not a discount Function. It can only discount existing lines. A cart transform can expand a purchased product into extra component lines, and the theme or Storefront API can add the gift as its own line.

### What if the customer removes items after the gift is added?

The Function runs again and stops discounting the gift, so it’s charged. Have the theme remove it, or tell the customer, to avoid surprises.

### Can I limit the gift to one per order?

Yes, this code already discounts one unit. To prevent extra units, also cap the quantity in the theme.

## Shopify documentation

- [Discount Function API](https://shopify.dev/docs/api/functions/latest/discount)
- [Cart Transform Function API](https://shopify.dev/docs/api/functions/latest/cart-transform)

## Related

- [Turn on a discount Function as an automatic discount](https://lintelagency.com/resources/graphql/function-backed-automatic-discount)
- [Expand a bundle into its components](https://lintelagency.com/resources/shopify-functions/expand-bundle-into-components)
- [Bundles with Cart Transform, and what needs Plus](https://lintelagency.com/journal/shopify-bundles-cart-transform)
- [Buy X get Y discount](https://lintelagency.com/resources/shopify-functions/buy-x-get-y-discount)

License: MIT. Free to use in your own store and in client work.
