# 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.

Page: https://lintelagency.com/resources/shopify-functions/personalization-fee-line-update

## 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.

## Facts

- **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:** 2 November 2026

## 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

```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

```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

```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

```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

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

### tests/converted-fee.input.json

```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

```json
{
  "operations": [
    {
      "lineUpdate": {
        "cartLineId": "gid://shopify/CartLine/1",
        "title": "Leather Wallet · monogrammed",
        "price": { "adjustment": { "fixedPricePerUnit": { "amount": "90.00" } } }
      }
    }
  ]
}
```

### tests/engraved-line.input.json

```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

```json
{
  "operations": [
    {
      "lineUpdate": {
        "cartLineId": "gid://shopify/CartLine/1",
        "title": "Brass Pocket Watch (engraved)",
        "price": { "adjustment": { "fixedPricePerUnit": { "amount": "59.00" } } }
      }
    }
  ]
}
```

## 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.

## Shopify documentation

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

## Related

- [Personalization from Product Page to Cart in Shopify](https://lintelagency.com/journal/product-personalization-liquid)
- [Which Shopify Functions capabilities need Shopify Plus](https://lintelagency.com/journal/shopify-functions-plus-requirements)
- [Bundles with Cart Transform, and what needs Plus](https://lintelagency.com/journal/shopify-bundles-cart-transform)

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