Developer guide
Shopify Scripts to Shopify Functions migration guide
A technical Shopify developer guide to migrating line item, shipping, and payment Scripts to Shopify Functions, unified Discount API, delivery and payment customizations, validation, and modern deployment workflows.
Why this migration is harder than it looks
Shopify Scripts migrations get underestimated because teams remember them as a code rewrite. In reality, most production migrations are architecture rewrites. A single Script often mixed several concerns at once: discounting, conditional logic, shipping control, payment handling, or cart behavior. Shopify Functions break those concerns into explicit APIs with clear surfaces, stricter limits, and app-based activation.
That is a good long-term model, but it means many existing Script implementations are not portable line by line. A line item Script that adjusted prices, rejected discount codes, and modified cart-line behavior was never really one feature. It was several features hidden in one Ruby file.
“Shopify Scripts will be sunset on June 30, 2026. All existing Shopify Scripts will stop functioning after this date.”
The most expensive mistake
The most expensive mistake is starting from the old Script file and trying to port it mechanically. Start from business behavior instead: what exact checkout behavior must still exist after Scripts are gone?
What actually changes in the execution model
The core mental shift is this: Shopify Scripts were an imperative mutation model, while Shopify Functions are a deterministic operations model.
With Scripts, developers often thought in terms of mutating cart or checkout state in place. With Functions, Shopify runs an explicit GraphQL input query, passes only the requested data into the function, and expects the function to return structured operations for a specific target.
This has several consequences:
- you only receive the fields you ask for in the input query
- your function must be deterministic
- you cannot rely on random values or the current time inside the function
- you cannot debug production behavior by printing to stdout or stderr
- you must stay within instruction, memory, and query-cost limits
“Shopify doesn’t allow nondeterminism in functions, which means that you can’t use any randomizing or clock functionality in your functions.”
That is why a Script migration often fails on the second or third edge case, not on the first happy path. The function runs in a much tighter environment, so any migration that quietly depends on mutable state, broad object access, or ad hoc debug output will need a redesign.
Deadlines, availability, and plan constraints
There are three constraints developers need to keep straight.
- Shopify Scripts stop working on June 30, 2026.
- Scripts and the Script Editor app were a Shopify Plus feature.
Public apps that contain Functions can be used on any plan where the relevant Function capability is supported, but custom apps that contain Function APIs are a Shopify Plus-only path.
That last point matters more than many teams realize. A lot of old Script logic was effectively store-specific private logic. If you rebuild it as a custom app, the plan requirement still matters. If you rebuild it into a public app, the availability story changes, but so does your product and deployment model.
“Stores on any plan can use public apps that are distributed through the Shopify App Store and contain functions. Only stores on a Shopify Plus plan can use custom apps that contain Shopify Function APIs.”
Developers should also keep current per-API activation limits in view. Shopify’s docs currently say stores can activate up to 25 discount functions, 25 delivery customization functions, 25 payment customization functions, and 25 validation functions, while Cart Transform is limited to one installed function per store.
Map each Script type to the right Function API
Shopify’s own migration guide gives the right top-level mapping:
- line item scripts -> Discount API
- line item scripts -> Cart Transform API
- line item scripts -> Cart and Checkout Validation API
- shipping scripts -> Delivery Customization API and Discount API
- payment scripts -> Payment Customization API
That mapping is important because legacy Scripts usually overloaded “discounting” to solve problems that were not really discounts. If the old Script was splitting lines, injecting bundle structure, or blocking invalid combinations, the better destination might be Cart Transform or Validation rather than Discount.
If the migration includes bundle assembly or cart-line reshaping, the
Cart Transform Function deep dive
covers the constraints and discount interactions in more detail. If the broader project also includes legacy checkout surfaces, pair this work with the
Shopify Checkout UI Extensions migration guide
.
A practical classification table looks like this:
| Legacy behavior | Modern target | Why |
|---|---|---|
| Reduce line price or subtotal | Discount Function API | Native discount operations and stacking model |
| Apply shipping-rate discount | Discount Function API | Delivery discounts are now part of the unified discount surface |
| Hide, rename, reorder delivery options | Delivery Customization API | Checkout delivery method transformation, not discounting |
| Hide, rename, reorder payment methods | Payment Customization API | Checkout payment transformation |
| Bundle, merge, add items | Cart Transform API | Structural cart manipulation |
| Block checkout on invalid conditions | Cart and Checkout Validation API | Rule enforcement with targetable error messages |
“Use the following table to map your existing Shopify Script to the appropriate Function API.”
Audit the existing Script before touching code
Shopify’s Help Center recommends using the Shopify Scripts customizations report before migrating. That is a good start, but it is not enough for real-world work. You still need your own engineering audit.
For each Script or Script behavior, capture:
- script type: line item, shipping, or payment
- business goal: discounting, transformation, validation, or method control
- surface: cart, checkout, shipping methods, payment methods
- inputs used: customer tags, collections, variants, addresses, cart total
- merchant configuration dependencies
- stacking assumptions with other discounts or customizations
- parity risk: direct mapping, redesign required, or not supported
A lot of migrations become simpler once you split one Script into 3 to 6 distinct behaviors with separate owners. That decomposition usually exposes dead logic, duplicate logic, and behaviors that should not be carried forward.
Migration inventory example
1. VIP line discount for tagged customers
-> Discount Function API
2. Hide express shipping for PO boxes
-> Delivery Customization API
3. Hide invoice payment unless order total > 1000
-> Payment Customization API
4. Reject discount code when cart contains restricted SKUs
-> Discount Function API (enteredDiscountCodesReject)
5. Block checkout if mixed hazardous and non-hazardous items
-> Cart and Checkout Validation APIScaffold the correct Function extension
Shopify Functions are scaffolded with Shopify CLI. Each Function extension has a TOML config, one or more targets, an input query, and the function implementation. That explicit structure is a major difference from the Script era.
A typical scaffold command looks like this:
shopify app generate extension --template discount --name vip-discountA discount extension config might look like this:
api_version = "2026-01"
[[extensions]]
name = "VIP discount"
handle = "vip-discount"
type = "function"
description = "Applies VIP discounts and optional shipping discounts"
[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
export = "cartLinesDiscountsGenerateRun"
[[extensions.targeting]]
target = "cart.delivery-options.discounts.generate.run"
input_query = "src/cart_delivery_options_discounts_generate_run.graphql"
export = "cartDeliveryOptionsDiscountsGenerateRun"One important design detail: targets are not just syntax. They are execution boundaries. If you need line and order discounts plus shipping discounts, your extension will usually implement separate run targets, not one monolithic function entry point.
Migrate a line item Script to the unified Discount Function API
Start with a classic line item Script example:
# Ruby Script Editor example
VIP_TAG = "VIP"
customer = Input.cart.customer
if customer && customer.tags.include?(VIP_TAG)
Input.cart.line_items.each do |line_item|
next if line_item.variant.product.gift_card?
new_price = line_item.line_price * 0.9
line_item.change_line_price(new_price, message: "VIP 10% off")
end
end
Output.cart = Input.cartIn the unified Discount Function model, the same behavior becomes an operation list driven by explicit input data. The first step is to request only the fields you actually need:
query CartLinesDiscountsGenerateRunInput {
cart {
buyerIdentity {
customer {
hasAnyTag(tags: ["VIP"])
}
}
lines {
id
quantity
merchandise {
__typename
... on ProductVariant {
id
product {
isGiftCard
}
}
}
}
}
discount {
discountClasses
}
}Then implement the function. The exact generated types differ by setup, but the core shape is an input object in and an operations array out.
// Illustrative JavaScript example for the unified Discount Function API
/**
* @param {Input} input
* @returns {CartLinesDiscountsGenerateRunResult}
*/
export function cartLinesDiscountsGenerateRun(input) {
const isVip = input.cart.buyerIdentity?.customer?.hasAnyTag === true;
if (!isVip) {
return { operations: [] };
}
const targets = input.cart.lines
.filter((line) => {
if (line.merchandise?.__typename !== "ProductVariant") return false;
return line.merchandise.product.isGiftCard !== true;
})
.map((line) => ({
cartLine: {
id: line.id,
},
}));
if (targets.length === 0) {
return { operations: [] };
}
return {
operations: [
{
productDiscountsAdd: {
candidates: [
{
message: "VIP 10% off",
targets,
value: {
percentage: {
value: 10.0,
},
},
},
],
selectionStrategy: "ALL",
},
},
],
};
}Two migration details matter here:
the Function is not mutating line prices directly, it is returning discount candidates
you should model “who qualifies” in the input query and function logic, not in hidden side channels
Shopify’s migration docs also call out two useful line-item mappings:
splitmaps to the optionalquantityfield on a product discount target for partial-quantity discountingrejectmaps toenteredDiscountCodesRejectin the new discount API
That is a subtle but important point. Many line item Scripts were really combining discounting with policy enforcement. Those pieces should be separated during the migration.
Migrate shipping Scripts
Shipping Scripts typically fall into two different buckets:
- discount the shipping rate
- hide, rename, or reorder available delivery options
Those are not the same migration.
If the old Script applied a discount to a shipping rate, move that logic into the unified Discount Function API’s delivery-discount target. If it changed which rates appeared or how they were labeled, use Delivery Customization instead.
A delivery customization input query can stay very lean:
query CartDeliveryOptionsTransformRunInput {
cart {
buyerIdentity {
customer {
hasAnyTag(tags: ["WHOLESALE"])
}
}
deliveryGroups {
deliveryAddress {
countryCode
zip
}
deliveryOptions {
handle
title
cost {
amount
}
}
}
}
}Example delivery customization logic:
// Illustrative JavaScript example for Delivery Customization
/**
* @param {Input} input
* @returns {CartDeliveryOptionsTransformRunResult}
*/
export function cartDeliveryOptionsTransformRun(input) {
const operations = [];
for (const group of input.cart.deliveryGroups) {
for (const option of group.deliveryOptions) {
if (group.deliveryAddress?.zip?.startsWith("PO")) {
if (option.title.toLowerCase().includes("express")) {
operations.push({
deliveryOptionHide: {
deliveryOptionHandle: option.handle,
},
});
}
}
if (option.title === "Standard") {
operations.push({
deliveryOptionRename: {
deliveryOptionHandle: option.handle,
title: "Standard Shipping (2 to 5 business days)",
},
});
}
}
}
return { operations };
}This is one of the easiest places to overuse the wrong API. If the merchant goal is “free shipping for VIP customers,” use a discount target. If the goal is “hide express for PO boxes,” use Delivery Customization. Combining them conceptually is fine, but combining them in one incorrect API is where migrations go wrong.
Migrate payment Scripts
Payment Scripts generally map much more cleanly to Payment Customization than line item Scripts map to Discount. The Payment Customization Function API is designed to rename, reorder, or hide payment methods, and for certain B2B flows it can also set payment terms or add review requirements.
“A payment customization enables you to rename, reorder, hide the payment methods available to customers during checkout, set payment terms, and add a review requirement for a specific order.”
Example input query:
query CartPaymentMethodsTransformRunInput {
cart {
cost {
totalAmount {
amount
}
}
buyerIdentity {
purchasingCompany {
company {
id
}
}
}
}
paymentMethods {
id
name
}
}Example payment customization:
// Illustrative JavaScript example for Payment Customization
/**
* @param {Input} input
* @returns {CartPaymentMethodsTransformRunResult}
*/
export function cartPaymentMethodsTransformRun(input) {
const total = Number(input.cart.cost.totalAmount.amount);
const isB2B = Boolean(input.cart.buyerIdentity?.purchasingCompany?.company?.id);
const operations = [];
for (const method of input.paymentMethods) {
if (!isB2B && method.name.includes("Invoice")) {
operations.push({
paymentMethodHide: {
paymentMethodId: method.id,
},
});
}
if (total > 1000 && method.name === "Bank transfer") {
operations.push({
paymentMethodRename: {
paymentMethodId: method.id,
name: "Bank transfer for high-value orders",
},
});
}
}
return { operations };
}One technical nuance from Shopify’s docs: storefront accelerated checkout support is only partial for payment customizations, and payment terms are not supported there. So if your legacy Script assumptions depended on uniform behavior across every payment surface, test those assumptions explicitly.
Use validation when the Script was really enforcing rules
Some Script logic looked like discounting, but functionally it was checkout policy enforcement. When the real business goal is “do not allow this order shape,” use Cart and Checkout Validation.
Shopify’s current validation docs are explicit that server-side validation is the way to enforce checks before allowing customers to proceed, including express checkout paths like Shop Pay, PayPal, Apple Pay, and Google Pay.
query CartValidationsGenerateRunInput {
cart {
lines {
quantity
merchandise {
__typename
... on ProductVariant {
product {
title
productType
}
}
}
}
}
}// Illustrative JavaScript example for Validation
/**
* @param {Input} input
* @returns {CartValidationsGenerateRunResult}
*/
export function cartValidationsGenerateRun(input) {
const hasHazmat = input.cart.lines.some(
(line) =>
line.merchandise?.__typename === "ProductVariant" &&
line.merchandise.product.productType === "Hazmat"
);
const hasFrozen = input.cart.lines.some(
(line) =>
line.merchandise?.__typename === "ProductVariant" &&
line.merchandise.product.productType === "Frozen"
);
if (hasHazmat && hasFrozen) {
return {
operations: [
{
validationAdd: {
message:
"Hazmat and frozen items cannot be purchased in the same checkout.",
target: "cart",
},
},
],
};
}
return { operations: [] };
}This is usually cleaner than trying to encode the same business rule into a discount rejection or awkward shipping/payment side effect.
Deployment, activation, and safe production testing
Shopify’s migration guide includes a very practical rollout pattern that many teams miss: test your new Function alongside the existing Script using a preview URL and a customer-tag gate.
The preview URL approach works by creating a draft passthrough Script:
Output.cart = Input.cartShopify’s documented preview URL format is:
https://{your-store}.myshopify.com/admin/scripts/preview?script_id={script_id}Then gate your Function for a test cohort only:
const customer = input.cart.buyerIdentity?.customer;
const hasTesterTag = customer?.hasAnyTag ?? false;
if (!hasTesterTag) {
return { operations: [] };
}
// apply new Function logic for TESTER-tagged users onlyThis is a smart migration pattern because it lets the legacy Script remain the default behavior while a controlled set of sessions exercises the new Function in production.
For activation without a merchant UI, Shopify’s migration guide also documents listing available Functions via GraphQL and then using the appropriate creation mutation:
query getFunctions {
shopifyFunctions(first: 100) {
edges {
node {
id
title
apiType
}
}
}
}Example automatic discount creation:
mutation CreateAutomaticDiscount($functionId: String!) {
discountAutomaticAppCreate(
automaticAppDiscount: {
title: "VIP 10% off"
functionId: $functionId
startsAt: "2026-03-10T00:00:00Z"
combinesWith: {
orderDiscounts: true
productDiscounts: true
shippingDiscounts: true
}
}
) {
automaticAppDiscount {
discountId
title
}
userErrors {
field
message
}
}
}One very common trap is trying to reference a function owned by another app. Shopify
docs explicitly say apps can reference only their own Functions in Admin GraphQL
mutations, otherwise you hit a Function not found error.
Configuration, metafields, and input-query design
Script Editor logic often mixed hardcoded constants with limited merchant controls. In the Functions model, configuration is usually better handled explicitly, either with merchant UI and app-managed metafields or with a more constrained GraphQL creation flow when no ongoing merchant configuration is needed.
Shopify’s docs repeatedly point developers toward metafields for flexible configuration of functions such as delivery customizations and discounts. That is a much cleaner pattern than baking merchant-specific thresholds and titles directly into source code.
Example input query that pulls configuration from a discount metafield:
query CartLinesDiscountsGenerateRunInput {
discount {
metafield(namespace: "$app:vip", key: "function-configuration") {
jsonValue
}
}
cart {
buyerIdentity {
customer {
hasAnyTag(tags: ["VIP"])
}
}
lines {
id
merchandise {
__typename
... on ProductVariant {
product {
isGiftCard
}
}
}
}
}
}Original insight: input-query design becomes part of your performance budget. In the Script era you often grabbed broad objects by default. In Functions, every extra field, metafield, or high-cost query pattern should justify itself.
Performance, limits, and why Rust usually wins
This is the part many migration guides skip, and it is where complex Script ports start failing in production.
Shopify’s Functions docs currently list hard limits such as:
- compiled binary size: 256 kB
- runtime linear memory: 10,000 kB
- runtime stack memory: 512 kB
- up to 11 million instructions for carts with up to 200 line items
- input query max size: 3000 bytes
- function input max: 128 kB
- function output max: 20 kB
- input query calculated cost max: 30
Those limits turn language choice into a real architecture decision. Shopify’s JavaScript docs say JavaScript is a good starting point for prototyping, but that you should expect to hit instruction limits sooner than equivalent Rust code. Shopify’s Rust docs then go further and say migrating from JavaScript to Rust can significantly improve performance and help you stay within platform fuel limits.
“For prototyping ideas, JavaScript is a good starting point if you’re familiar with the language. However, expect to run into instruction limits sooner than if you wrote the equivalent function logic in a language that compiles to WebAssembly directly, such as Rust.”
My technical recommendation is simple:
- use JavaScript for fast prototypes and small internal migrations
- default to Rust for high-volume stores or complex rule engines
- keep input queries narrow
- move merchant-configurable thresholds into metafields, not hardcoded branches
- avoid giant candidate lists and over-broad scans where target selection can be narrowed earlier
If a Script migration contains nested loops over all lines, collections, tags, and variants, it should trigger an early Rust discussion.
What does not have direct parity
Some Script behaviors do not have a clean one-to-one replacement.
Shopify’s migration docs explicitly call out that change_properties is
not available as a Script-to-Function equivalent. The docs point developers toward
applyAttributeChange in Checkout UI Extensions for attribute updates.
That is a good example of a deeper rule: if the old Script reached beyond pricing or method control into UI-adjacent or mutation-heavy behavior, the replacement may not live inside Functions at all.
Other non-parity areas usually include:
- overly broad cart mutation assumptions
- implicit interaction with merchant discount setup that is now governed by discount nodes and combination rules
- “just inspect everything” logic that now exceeds query-cost or instruction limits
- logic that depended on debugging by runtime print statements
Parity is the wrong success metric
The goal is not perfect Script emulation. The goal is preserving the business outcome using supported APIs, current discount architecture, and a function design that can survive future Shopify releases.
Original migration insights that save time
The following patterns are not just technical niceties. They usually save weeks on real migrations.
Do not port old discount logic into already-deprecated discount APIs. If you are touching Script discount logic in 2026, migrate directly to the unified Discount Function API.
Separate discounts from enforcement. A lot of Script code used price changes to simulate business rules. Put rule enforcement in Validation when that is the actual intent.
Separate shipping method control from shipping discounts. Rename, hide, and order delivery methods with Delivery Customization. Discount them with Discount Functions.
Build the audit first, not the code first. The migration inventory is often the highest-leverage artifact in the whole project.
Treat input-query design as architecture. Query only the fields needed for decisions. Over-querying is the quietest way to make a Function brittle.
Prefer Rust sooner than you think. If the old Script encoded serious merchant-specific pricing logic, JavaScript can become the second migration.
The best migration projects are the ones that end with fewer behaviors than they started with, because dead logic was removed and the surviving logic was assigned to the correct modern target.
Best internal links
These pages are the most useful follow-on reads after a Script audit because they cover the two migration boundaries teams usually hit next: cart structure and checkout extensibility.
Cart Transform Function deep dive: bundles, line expansion, merge logic, and when it conflicts with discounts
for Script behaviors that were really bundle construction, merge logic, or cart repricing problems instead of discount-only work.
Shopify Checkout UI Extensions migration guide
for legacy checkout.liquid, additional scripts, Thank you page, and pixel migrations that usually sit beside a Functions rollout.
Shopify analytics playbook for operators
for checking event parity after moving discount, shipping, or post-purchase logic off legacy checkout code.
Sources and further reading
Shopify Dev, Migrating from Shopify Scripts to Shopify Functions
Shopify Help Center, Transitioning from Shopify Scripts to Shopify Functions
Shopify Help Center, Shopify Scripts requirements and limitations
Shopify Dev, About Shopify Functions
Shopify Dev, Function APIs
Shopify Dev, JavaScript for Functions
Shopify Dev, Rust for Functions
Shopify Dev, Discount Function API
Shopify Developer Changelog, Introducing the new Discount Function API
Shopify Developer Changelog, Deprecation of Product, Order, and Shipping Discount Function APIs
Shopify Dev, Delivery Customization Function API
Shopify Dev, Payment Customization Function API
Shopify Dev, Cart and Checkout Validation Function API
Shopify Dev, Build a Discount Function
Shopify Dev, Build the delivery options function
Shopify Dev, Create the payments function
Shopify Dev, shopifyFunctions query
Shopify Dev, discountAutomaticAppCreate
Shopify Dev, deliveryCustomizationCreate
Shopify Dev, paymentCustomizationCreate
FAQ
Is Shopify Scripts migration just a switch from Ruby to JavaScript?
No. The language change is the easy part. The real migration is from an imperative Script model that mutates checkout behavior directly to a deterministic function model that returns operations for specific extension targets. Many Script migrations also span several APIs, such as Discount, Delivery Customization, Payment Customization, Cart Transform, or Validation.
Should I migrate old script discounts to the older product, order, or shipping discount Function APIs?
No, not for a new migration in 2026. Shopify introduced the unified Discount Function API in API version 2025-04 and deprecated the older Product, Order, and Shipping Discount Function APIs. For most new Script migrations, the better target is the unified Discount Function API.
Can I keep using JavaScript for Shopify Functions in production?
Yes, but Shopify strongly recommends Rust for production-grade Functions. JavaScript is often fine for prototyping or lighter logic, but more complex Functions can hit fuel or instruction limits sooner than equivalent Rust implementations.
What is the hardest part of a real migration?
Usually not syntax. The hard part is preserving business behavior under a stricter execution model. Teams often need to redesign assumptions around mutable cart state, dynamic line properties, discount stacking, payment and shipping ordering, and post-deployment configuration.
Related resources
Keep exploring the playbook
Cart Transform Function deep dive: bundles, line expansion, merge logic, and when it conflicts with discounts
A technical Shopify developer guide to Cart Transform Functions covering lineExpand, linesMerge, lineUpdate, bundle pricing, metafield-driven configuration, discount conflicts, limits, and production design patterns.
Shopify Checkout UI Extensions migration guide
A practical Shopify developer guide to moving from checkout.liquid, Scripts, additional scripts, and script tags to Checkout UI Extensions, Functions, web pixels, and blocks.
Shopify AEO guide
A practical Shopify guide to answer engine optimization covering AI search visibility, product citability, structured data, Shopify Catalog, entity signals, and measurement.