Developer reference

Shopify Cart Transform Functions — Complete Developer Guide

Cart Transform Functions run inside Shopify's checkout runtime to modify cart lines — add fees, split into components, merge into bundles. This is the full 2026 developer reference: what they do, when to use, how to deploy, and copy-paste code for a per-character pricing implementation.

Last updated: August 14, 2026~11 min readBy the Print It My Way team

What Cart Transform Functions do

A Cart Transform Function runs inside Shopify's checkout when a cart is loaded or updated. Given the current cart lines as input, your function returns a set of operations that Shopify applies before rendering checkout to the customer. Three operation types exist:

The most common use case in personalization is Update — adding a per-option surcharge to a cart line based on its line-item properties. Instead of creating a hidden "engraving fee" product and adding it to the cart (the old pattern), Cart Transform lets you dynamically compute the fee at checkout time from the customer's actual input.

When Cart Transform beats the alternatives

ApproachProsCons
Cart TransformClean cart line, no hidden products, runs server-side, immune to discount code manipulationRust/JS + CLI deploy required; Plus-native only
Hidden fee productWorks on all plans without FunctionClutters admin, breaks reports, breaks discount codes, visible in checkout as separate line
Variant per fee tierSimple to set upVariant explosion; not viable past a few tiers
Custom app checkout UI (Plus)Full controlRequires Checkout Extensions + more infrastructure

For non-Plus stores, apps like Print It My Way provide an equivalent mechanism via cart-line attribute manipulation. Customer experience is identical; implementation is app-side.

Setting up your environment

Prerequisites:

Create a new Shopify app scaffold:

shopify app init my-cart-transform-app
cd my-cart-transform-app
shopify app generate extension --type=cart_transform --name=fee-per-character --template=javascript

The template scaffolds an extensions/fee-per-character/ directory with:

Anatomy of a Cart Transform Function

Every Cart Transform Function has two files:

Input query (src/run.graphql) — declares what data your function needs from the cart:

query RunInput {
  cart {
    lines {
      id
      quantity
      cost {
        amountPerQuantity {
          amount
          currencyCode
        }
      }
      merchandise {
        ... on ProductVariant {
          id
          product { id title }
        }
      }
      attribute(key: "Engraving Text") {
        value
      }
    }
  }
}

Runner (src/run.js) — the function that receives the input and returns operations:

// @ts-check

/**
 * @typedef {import("../generated/api").RunInput} RunInput
 * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult
 */

const NO_CHANGES = { operations: [] };
const FREE_CHARS = 10;
const PER_CHAR_FEE = 0.50;

/**
 * @param {RunInput} input
 * @returns {FunctionRunResult}
 */
export function run(input) {
  const operations = [];

  for (const line of input.cart.lines) {
    const engravingText = line.attribute?.value;
    if (!engravingText) continue;

    const charCount = engravingText.length;
    const extraChars = Math.max(0, charCount - FREE_CHARS);
    if (extraChars === 0) continue;

    const feePerUnit = extraChars * PER_CHAR_FEE;
    const basePriceCents = Math.round(
      parseFloat(line.cost.amountPerQuantity.amount) * 100
    );
    const newPriceCents = basePriceCents + Math.round(feePerUnit * 100);
    const newPrice = (newPriceCents / 100).toFixed(2);

    operations.push({
      lineUpdate: {
        cartLineId: line.id,
        price: {
          adjustment: {
            fixedPricePerUnit: {
              amount: newPrice,
            },
          },
        },
        title: `${line.merchandise.product.title} — Engraved`,
      },
    });
  }

  return operations.length ? { operations } : NO_CHANGES;
}
Critical: Cart Transform Functions must execute in under ~5ms. Keep logic pure JavaScript, avoid loops over more than a few dozen lines, and never call external APIs (not allowed in the runtime anyway). Compile to WASM — Shopify caps at 256KB compiled size.

Deploying your function

Register the extension on your app and push it live:

# Local dev with hot reload
shopify app dev

# When ready, deploy to production
shopify app deploy

After deploy, merchants who install your app can enable the Cart Transform via the app's admin UI. Programmatically, you activate it via the Admin API:

mutation ActivateCartTransform($functionId: String!) {
  cartTransformCreate(functionId: $functionId) {
    cartTransform { id }
    userErrors { field message }
  }
}

Only one Cart Transform can be active per store at a time. If a merchant already has one, they'll need to deactivate the old before enabling yours.

Rust version of the same function

Rust compiles smaller + runs faster than JavaScript in WASM. For high-traffic stores, Rust is the sensible choice:

use shopify_function::prelude::*;
use shopify_function::Result;

generate_types!(query_path = "src/run.graphql", schema_path = "schema.graphql");

const FREE_CHARS: usize = 10;
const PER_CHAR_FEE_CENTS: u64 = 50;

#[shopify_function]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
    let mut operations = Vec::new();

    for line in input.cart.lines {
        let engraving = match line.attribute.as_ref().and_then(|a| a.value.as_ref()) {
            Some(v) => v,
            None => continue,
        };

        let char_count = engraving.chars().count();
        if char_count <= FREE_CHARS { continue; }

        let extra = char_count - FREE_CHARS;
        let fee_cents = extra as u64 * PER_CHAR_FEE_CENTS;

        let base: f64 = line.cost.amount_per_quantity.amount.parse()?;
        let base_cents = (base * 100.0).round() as u64;
        let new_cents = base_cents + fee_cents;
        let new_price = format!("{}.{:02}", new_cents / 100, new_cents % 100);

        operations.push(output::Operation {
            line_update: Some(output::LineUpdateOperation {
                cart_line_id: line.id,
                price: Some(output::PriceAdjustment {
                    adjustment: output::PriceAdjustmentValue::FixedPricePerUnit(
                        output::FixedPricePerUnit { amount: new_price }
                    ),
                }),
                title: Some(format!("Engraved item")),
            }),
            ..Default::default()
        });
    }

    Ok(output::FunctionRunResult { operations })
}

Testing your function

Shopify CLI provides a local test runner:

# Run the function against a sample input
shopify app function run

# With custom input JSON
shopify app function run --input=./test/sample-input.json

Create test/sample-input.json matching your query schema:

{
  "cart": {
    "lines": [
      {
        "id": "gid://shopify/CartLine/1",
        "quantity": 1,
        "cost": { "amountPerQuantity": { "amount": "25.00", "currencyCode": "USD" } },
        "merchandise": {
          "__typename": "ProductVariant",
          "id": "gid://shopify/ProductVariant/123",
          "product": { "id": "gid://shopify/Product/456", "title": "Silver Necklace" }
        },
        "attribute": { "value": "Emma Johnson" }
      }
    ]
  }
}

For "Emma Johnson" (12 chars), function should add (12-10) × $0.50 = $1.00 fee, returning price $26.00.

Common Cart Transform patterns

Pattern 1: Per-option flat fee

Customer selects "Gift Wrap" → add $5. Look for the attribute in each line, add fixed amount.

Pattern 2: Bundle expand

Customer buys "Starter Kit" (single SKU) → expand into 3 component SKUs in the cart. Uses lineExpand operation. POD partners handle each component separately.

Pattern 3: Tier discount

Cart has 10+ personalized units → apply -10% to each. Compute quantity across matching lines, apply price adjustment.

Pattern 4: Rush production surcharge

Line has attribute Delivery Date < 5 days out → add +20% rush fee. Parse date, compute delta from current UTC.

Watch out: Cart Transform runs before Shopify Discounts. If you add a fee and a merchant runs a "20% off cart" discount, your fee gets discounted too unless you tag it via attribute with a discount-exclusion flag and pair with a Discount Function that skips flagged lines.

Rate limits + execution constraints

ConstraintLimit
Execution time~5 ms per invocation (soft), 20 ms hard limit
Memory10 MB heap
Compiled WASM size256 KB
Cart lines per invocationPractically unbounded (up to Shopify cart limit)
External API callsNot allowed — pure computation only
Random / dateAvailable; deterministic seeds

Cart Transform vs Discount Functions

They're often confused:

If you want a per-option surcharge, use Cart Transform. If you want to run a "buy 3, get 1 free" promotion, use Discount Functions. Both can coexist on the same store; Cart Transform runs first.

Non-Plus stores

Cart Transform Functions are Shopify Plus only. On non-Plus stores, apps achieve equivalent behavior via a different mechanism — cart-attribute manipulation, hidden fee products, or draft-order creation at checkout. Print It My Way handles both Plus (native Cart Transform) and non-Plus (equivalent app-side) transparently — customer experience is identical, developer doesn't have to care about which plan the merchant is on.

Common errors

ErrorCauseFix
Function exceeds execution timeLoop over too many lines or heavy computationCap iteration; move logic to app-side pre-computation stored in attribute
Function exceeds WASM sizeLarge dependencies compiled inRust: enable opt-level = "z" + lto = true. JS: tree-shake with esbuild
Operations array empty but expected changesAttribute key mismatch (case-sensitive)Verify exact key from your personalizer
Fee appears twice on lineFunction ran multiple times without idempotency checkUse _fee_applied hidden attribute; skip line if already flagged
Discount doesn't apply after Cart TransformFee added to same line prevents discount from stacking cleanlyAdd fee as separate line-item property + coordinate with Discount Function

Frequently asked questions

What are Shopify Cart Transform Functions?

Server-side functions that run at checkout to modify cart lines — add fees, split into components, merge into bundles. Native Shopify API since 2023. Plus-only.

Are Cart Transform Functions Plus-only?

Native yes. Non-Plus stores achieve equivalent behavior via app-side cart-attribute manipulation. Print It My Way and similar apps handle both transparently.

How do I deploy a Cart Transform Function?

Shopify CLI 3.x + shopify app generate extension --type=cart_transform. Write code, test locally with shopify app function run, deploy with shopify app deploy.

Best language — Rust or JavaScript?

Rust for performance-critical or high-cart-line scenarios. JavaScript for prototypes or simple logic. Both compile to WASM. Rust ~2-5x faster in Shopify's runtime.

What's the execution time limit?

~5 ms soft, 20 ms hard. Keep logic pure computation; no external API calls allowed.

Can multiple Cart Transform Functions coexist?

Only one active per store at a time. Merchant must deactivate old before enabling new.

How does Cart Transform interact with discount codes?

Cart Transform runs before Discounts. Fees you add via Cart Transform will be discounted by percentage-off codes unless you flag them for exclusion.

Cart Transform vs Checkout Extensions?

Cart Transform: modify prices/lines server-side. Checkout Extensions: custom UI on checkout page. Different use cases; can coexist.

How to test Cart Transform locally?

shopify app function run --input=./test/sample-input.json. CLI simulates the runtime and shows returned operations.

Cart Transform vs Discount Functions?

Cart Transform modifies price/title/attributes/structure. Discount Functions apply named percentage or fixed discounts. Use whichever matches your intent.

Related developer references

Personalization + Cart Transform, done for you

Print It My Way ships Cart Transform Functions for per-character, per-option, and tiered pricing — no code required. Free plan available.

Install Print It My Way