Personalization fee with lineUpdate

A Shopify cart transform Function that adds an engraving fee to the line price and title when a line item property is filled in. Shopify Plus.

Function API
Cart Transform
Target
cart.transform.run
Operation
lineUpdate
Plan
Shopify Plus or development store
App scope
write_cart_transforms
API version
2026-10
Last checked

In short

This cart transform Function adds a personalization fee, 10 in your store currency by default, to the unit price of any line with an engraving, and marks the title. The engraving comes from a line item property. It uses the lineUpdate operation of the cart.transform.run target, Cart Transform API version 2026-10, which Shopify allows only on Shopify Plus and development stores.

How to use it

  1. Add a text field to the product form: <input name="properties[Engraving]">.
  2. Create an app with the Shopify CLI, add the extension (shopify app generate extension --template cart_transform) and replace its files with the files below.
  3. Run the fixtures with shopify app function run --input tests/engraved-line.input.json, then deploy.
  4. Activate it with cartTransformCreate and set {"fee": 10, "titleSuffix": " (engraved)"} in its metafield if you want other values.
  5. Show item.title in the cart template so the new title appears.

Code

shopify.extension.toml
api_version = "2026-10"

[[extensions]]
name = "Personalization fee with lineUpdate"
handle = "personalization-fee-line-update"
type = "function"

  [[extensions.targeting]]
  target = "cart.transform.run"
  input_query = "src/cart_transform_run.graphql"
  export = "cart-transform-run"

  [extensions.build]
  command = ""
  path = "dist/function.wasm"
src/cart_transform_run.graphql
query CartTransformRunInput {
  presentmentCurrencyRate
  cart {
    lines {
      id
      engraving: attribute(key: "Engraving") {
        value
      }
      cost {
        amountPerQuantity {
          amount
        }
      }
      merchandise {
        __typename
        ... on ProductVariant {
          product {
            title
          }
        }
      }
    }
  }
  cartTransform {
    metafield(namespace: "$app", key: "function-configuration") {
      jsonValue
    }
  }
}
src/cart_transform_run.js
// @ts-check

/**
 * @typedef {import("../generated/api").CartTransformRunInput} CartTransformRunInput
 * @typedef {import("../generated/api").CartTransformRunResult} CartTransformRunResult
 */

/**
 * Settings stored as JSON in the cart transform's metafield. The engraving
 * text comes from a line item property named "Engraving" on the product form
 * (<input name="properties[Engraving]">); change the attribute key in the
 * query if your theme uses another name.
 */
const DEFAULT_CONFIG = {
  // Added to each engraved unit, in the store's currency.
  fee: 10,
  // Appended to the product title on engraved lines.
  titleSuffix: " (engraved)",
};

/**
 * @param {CartTransformRunInput} input
 * @returns {CartTransformRunResult}
 */
export function cartTransformRun(input) {
  const config = { ...DEFAULT_CONFIG, ...input.cartTransform?.metafield?.jsonValue };
  // Prices in the input and output are in the customer's currency.
  const fee = config.fee * Number(input.presentmentCurrencyRate);

  const operations = input.cart.lines
    .filter((line) => line.merchandise.__typename === "ProductVariant" && (line.engraving?.value ?? "").trim() !== "")
    .map((line) => {
      const title = line.merchandise.__typename === "ProductVariant" ? line.merchandise.product.title : "";
      // A fixed unit price replaces the variant price, so it includes the fee.
      const unitPrice = Number(line.cost.amountPerQuantity.amount) + fee;
      return {
        lineUpdate: {
          cartLineId: line.id,
          title: `${title}${config.titleSuffix}`,
          price: { adjustment: { fixedPricePerUnit: { amount: unitPrice.toFixed(2) } } },
        },
      };
    });

  return { operations };
}
Test input and expected output
tests/blank-engraving.input.json
{
  "presentmentCurrencyRate": "1.0",
  "cart": {
    "lines": [
      {
        "id": "gid://shopify/CartLine/1",
        "engraving": { "value": "   " },
        "cost": { "amountPerQuantity": { "amount": "49.0" } },
        "merchandise": { "__typename": "ProductVariant", "product": { "title": "Brass Pocket Watch" } }
      }
    ]
  },
  "cartTransform": { "metafield": null }
}
tests/blank-engraving.output.json
{ "operations": [] }
tests/converted-fee.input.json
{
  "presentmentCurrencyRate": "1.25",
  "cart": {
    "lines": [
      {
        "id": "gid://shopify/CartLine/1",
        "engraving": { "value": "JS" },
        "cost": { "amountPerQuantity": { "amount": "80.0" } },
        "merchandise": { "__typename": "ProductVariant", "product": { "title": "Leather Wallet" } }
      }
    ]
  },
  "cartTransform": { "metafield": { "jsonValue": { "fee": 8, "titleSuffix": " · monogrammed" } } }
}
tests/converted-fee.output.json
{
  "operations": [
    {
      "lineUpdate": {
        "cartLineId": "gid://shopify/CartLine/1",
        "title": "Leather Wallet · monogrammed",
        "price": { "adjustment": { "fixedPricePerUnit": { "amount": "90.00" } } }
      }
    }
  ]
}
tests/engraved-line.input.json
{
  "presentmentCurrencyRate": "1.0",
  "cart": {
    "lines": [
      {
        "id": "gid://shopify/CartLine/1",
        "engraving": { "value": "For Ada, 2026" },
        "cost": { "amountPerQuantity": { "amount": "49.0" } },
        "merchandise": { "__typename": "ProductVariant", "product": { "title": "Brass Pocket Watch" } }
      },
      {
        "id": "gid://shopify/CartLine/2",
        "engraving": null,
        "cost": { "amountPerQuantity": { "amount": "49.0" } },
        "merchandise": { "__typename": "ProductVariant", "product": { "title": "Brass Pocket Watch" } }
      }
    ]
  },
  "cartTransform": { "metafield": null }
}
tests/engraved-line.output.json
{
  "operations": [
    {
      "lineUpdate": {
        "cartLineId": "gid://shopify/CartLine/1",
        "title": "Brass Pocket Watch (engraved)",
        "price": { "adjustment": { "fixedPricePerUnit": { "amount": "59.00" } } }
      }
    }
  ]
}

Written by Lintel. MIT license: free to use in your own store and in client work.

Limits and edge cases

  • lineUpdate is rejected on stores that aren’t on Shopify Plus or a development store.
  • A line that’s also being expanded or merged can’t be updated; Shopify drops the lineUpdate.
  • The fee is converted with presentmentCurrencyRate; prices in the input and output are in the customer’s currency.
  • Titles changed by a Function show in the cart, checkout and notification emails only where the template uses item.title or line.title.

Questions

How do I charge extra for engraving on Shopify?
On Shopify Plus, a cart transform with lineUpdate can raise the price of the engraved line, as this example does. On other plans, add a separate fee product to the cart.
Why not change the price in the theme?
Theme prices are only display. The price customers pay is set server-side, so it has to come from a variant, a discount or a cart transform.
What if my store isn’t on Shopify Plus?
Use a separate “engraving fee” product that the theme adds to the cart together with the engraved item.
  • Written for Function API version 2026-10 and checked against Shopify’s reference on the date shown. Shopify releases a new version every quarter.
  • Custom apps that contain Functions only run on Shopify Plus stores; a public app with the same Function runs on any plan unless the page says otherwise.
  • Every example ships with test fixtures, and we run them before publishing. Run them again after you change the code: shopify app function run.
  • MIT license. Use the code in your own store or in client work.

Need this built, tested and maintained for your store? See Shopify Functions development.

Have something to build? Let’s talk it through.

Taking a limited number of projects at a time. Every brief is read by an engineer, not a salesperson.

Gencer KarakayaShopify Engineer

An engineer reads your brief, and we reply by email.