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:
- Update: modify a cart line's price, title, or attributes
- Expand: split one cart line into multiple (e.g. bundle → components)
- Merge: combine multiple cart lines into one (e.g. components → bundle)
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
| Approach | Pros | Cons |
|---|---|---|
| Cart Transform | Clean cart line, no hidden products, runs server-side, immune to discount code manipulation | Rust/JS + CLI deploy required; Plus-native only |
| Hidden fee product | Works on all plans without Function | Clutters admin, breaks reports, breaks discount codes, visible in checkout as separate line |
| Variant per fee tier | Simple to set up | Variant explosion; not viable past a few tiers |
| Custom app checkout UI (Plus) | Full control | Requires 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:
- Shopify CLI 3.x installed:
npm install -g @shopify/cli @shopify/theme - Shopify Partners account + development store
- Node.js 18 or higher (for CLI + JS functions)
- Rust 1.75+ (optional; only if writing Rust functions)
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:
src/run.js— your function codesrc/run.graphql— GraphQL input schema (what your function receives)shopify.extension.toml— extension config
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;
}
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.
attribute with a discount-exclusion flag and pair with a Discount Function that skips flagged lines.
Rate limits + execution constraints
| Constraint | Limit |
|---|---|
| Execution time | ~5 ms per invocation (soft), 20 ms hard limit |
| Memory | 10 MB heap |
| Compiled WASM size | 256 KB |
| Cart lines per invocation | Practically unbounded (up to Shopify cart limit) |
| External API calls | Not allowed — pure computation only |
| Random / date | Available; deterministic seeds |
Cart Transform vs Discount Functions
They're often confused:
- Cart Transform: modify prices, split/merge lines, change titles + attributes
- Discount Functions: apply named discounts (percentage, fixed amount) to specific lines
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
| Error | Cause | Fix |
|---|---|---|
| Function exceeds execution time | Loop over too many lines or heavy computation | Cap iteration; move logic to app-side pre-computation stored in attribute |
| Function exceeds WASM size | Large dependencies compiled in | Rust: enable opt-level = "z" + lto = true. JS: tree-shake with esbuild |
| Operations array empty but expected changes | Attribute key mismatch (case-sensitive) | Verify exact key from your personalizer |
| Fee appears twice on line | Function ran multiple times without idempotency check | Use _fee_applied hidden attribute; skip line if already flagged |
| Discount doesn't apply after Cart Transform | Fee added to same line prevents discount from stacking cleanly | Add 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
- Cart Transform API — Deeper spec reference
- Storefront API for Variants
- GraphQL Admin API for Variants
- Per-Character Pricing (Merchant Setup)
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