Apr 16, 2026
How to Migrate Shopify Scripts to Functions: The Complete Code Tutorial (2026 Edition)
How to Migrate Shopify Scripts to Functions: The Complete Code Tutorial (2026 Edition)
How to Migrate Shopify Scripts to Functions: The Complete Code Tutorial (2026 Edition)

It's April 16, 2026. Yesterday — April 15 — was the day Shopify locked the Script Editor for good. You can no longer create or publish a new Script. The execution shutdown lands in 75 days, on June 30, 2026.
If you're a Shopify Plus developer — or the agency running a Plus store — who pushed this migration to "next sprint" for the last twelve months, you have a problem. Not a "nice to fix" problem. A "your checkout breaks at midnight on July 1" problem. Most Plus stores have accumulated 5 to 20 Scripts over the years, each quietly powering a discount rule, a shipping hide, or a payment gate that nobody remembers writing.
This guide is the technical migration manual we wished existed in January. It covers the actual code — not just the strategy. By the time you finish reading, you'll know how to scaffold a Function with the Shopify CLI, write the Rust or JavaScript logic for discounts, delivery customizations, and payment customizations, test it safely against a tagged subset of your customers, and ship it to production without breaking your existing checkout.
Let's get the Scripts off your store and the Functions on it.

Quick Answer: Scripts → Functions in 60 Seconds
The migration in one paragraph: Shopify Scripts (Ruby code in the Script Editor, Plus-only) are being replaced by Shopify Functions (WebAssembly modules written in Rust or JavaScript, available to all plans). You scaffold a Function with
shopify app generate extension, write arun.graphqlquery that pulls the cart data you need, write arun.rsorrun.jsfile that returns operations (discounts, hidden shipping methods, etc.), then deploy withshopify app deployand activate it via Admin or a GraphQL mutation. Functions execute as compiled WASM at sub-5ms latency, run on every plan, and are the only customization path Shopify supports going forward.
What's Actually Changing on June 30
Before we touch any code, get the dates straight. There are two of them, and both matter.
Date | What Happens | Your Action |
|---|---|---|
April 15, 2026 (passed) | Script Editor read-only. No new Scripts. No edits to existing Scripts. | Existing Scripts still execute. Migrate now or freeze your logic. |
June 30, 2026 | All Shopify Scripts stop executing. Period. | Your Function replacement must be live before this date. |
The migration is binary. Either your Function is deployed by June 30 and your checkout keeps working, or it isn't — and every affected cart quietly defaults back to standard pricing, standard shipping rates, and every payment method enabled. There's no partial credit. The Script either runs or it doesn't, and after June 30 it doesn't.
Tip: Open
Settings → Checkout → Customizations Reportin your Shopify admin. It lists every active Script in your store, what it does, and the recommended Function replacement type. Start there.
Functions vs Scripts: What Actually Changed
Dimension | Shopify Scripts (deprecated) | Shopify Functions (replacement) |
|---|---|---|
Language | Ruby DSL (Shopify-specific) | Rust, JavaScript, TypeScript |
Runtime | Sandboxed Ruby on Shopify infra | WebAssembly (WASM) — sub-5ms execution |
Plan availability | Plus only | All plans (custom apps require Plus; public apps are open) |
Editor | In-admin Script Editor | Local IDE + Shopify CLI |
Versioning | None — live edits | Git-friendly — full version control |
Testing | Manual in checkout | Local dev with |
Deployment | Click "Save" in admin |
|
Targets | Line items, shipping, payments | Discounts, Cart Transform, Validation, Delivery Customization, Payment Customization, Order Routing, Fulfillment Constraints, more |
The architectural shift matters. Scripts were "tweak Ruby in a textarea." Functions are "write a real app, version-control it, test it locally, deploy it through a real CI pipeline." That's a steeper on-ramp. It's also the last migration you'll do for checkout logic in the foreseeable future — Functions are Shopify's long-term commitment, not a stopgap like Scripts turned out to be.
Mapping Your Scripts to the Right Function Type
Every Script you have today maps to exactly one Function API. Here's the lookup table you need pinned to your monitor.
Old Script Type | What It Did | New Function API | Function Target |
|---|---|---|---|
Line Item Script | Apply discounts to specific products / customers / cart conditions | Cart & Checkout Discounts API |
|
Shipping Script (discount) | Free / discounted shipping based on cart rules | Cart & Checkout Discounts API |
|
Shipping Script (hide / rename / reorder) | Hide a shipping rate above $X, rename "Standard" to "Free over $50" | Delivery Customization API |
|
Payment Script | Hide PayPal for B2B, hide COD over $500, reorder methods | Payment Customization API |
|
Cart-modifying Script (rare) | Bundle products, swap line items | Cart Transform API |
|
Block-checkout Script | Reject cart if SKU mix invalid | Cart & Checkout Validation API |
|
If you have ten Scripts, you'll likely build three to five Functions — multiple Scripts often collapse into one Function with cleaner branching logic.

Prerequisite: Set Up Your Local Dev Environment
Before scaffolding any Function, you need three things installed locally. Run these checks in your terminal.
1. Node.js 18+
node --version # Must be >= 18.0.0
If older, install via nvm or download from nodejs.org.
2. Shopify CLI 3+
npm install -g @shopify/cli@latest shopify version # Should output 3.x or higher
3. Rust toolchain (only if you'll write Functions in Rust)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup target add wasm32-wasip1 cargo --version
JavaScript Functions don't need Rust. Pick one language for your team and stick with it — mixing both adds maintenance overhead.
4. A development store
Either log into your Partner dashboard and create a new development store, or use an existing one. You'll deploy Functions to this store before promoting to production.
Scaffolding Your First Function
The CLI does most of the boilerplate. Inside any directory:
# Create a new Shopify app (skip if you already have one) shopify app init my-checkout-functions cd my-checkout-functions # Generate a Function extension shopify app generate extension
The CLI walks you through prompts. For a discount Function, you'd pick:
Type: Function
Template:
discount(orcart_checkout_validation,delivery_customization,payment_customization, etc.)Language: Rust or JavaScript
Name: something like
volume-discount-fn
This creates extensions/volume-discount-fn/ with:
extensions/volume-discount-fn/ ├── shopify.extension.toml # Function config — targets, build, version ├── src/ │ ├── cart_lines_discounts_generate_run.graphql # Input query │ └── cart_lines_discounts_generate_run.rs # Function logic ├── Cargo.toml # Rust dependencies (Rust only) └── README.md
The three files you'll edit constantly are the .toml (config), the .graphql (input), and the .rs / .js (logic). That's it.
Tutorial 1: Replacing a Line Item Script (Volume Discount)
Say your old Script gave 10% off the order subtotal whenever the cart had 5+ units of a specific collection. Here's the Function equivalent.
Step 1.1: The configuration (shopify.extension.toml)
api_version = "2026-01" [[extensions]] name = "volume-discount-fn" handle = "volume-discount-fn" type = "function" [[extensions.targeting]] target = "cart.lines.discounts.generate.run" input_query = "src/cart_lines_discounts_generate_run.graphql" export = "cart_lines_discounts_generate_run" [extensions.build] command = "cargo build --target=wasm32-wasip1 --release" path = "target/wasm32-wasip1/release/volume-discount-fn.wasm" watch = ["src/**/*.rs"]
Step 1.2: The input query (src/cart_lines_discounts_generate_run.graphql)
query Input { cart { lines { id quantity cost { subtotalAmount { amount } } merchandise { ... on ProductVariant { product { inAnyCollection(ids: ["gid://shopify/Collection/123456789"]) } } } } } discount { discountClasses } }
Tip: Functions only see the data you query. Keep the GraphQL minimal — every field you skip means a faster, cheaper execution.
Step 1.3: The logic (src/cart_lines_discounts_generate_run.rs)
use super::schema; use shopify_function::prelude::*; use shopify_function::Result; #[shopify_function] fn cart_lines_discounts_generate_run( input: schema::cart_lines_discounts_generate_run::Input, ) -> Result<schema::CartLinesDiscountsGenerateRunResult> { // Bail if discount class doesn't match let has_order_discount = input .discount() .discount_classes() .contains(&schema::DiscountClass::Order); if !has_order_discount { return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] }); } // Sum quantities of items in the target collection let qualifying_qty: i64 = input .cart() .lines() .iter() .filter(|line| { if let schema::Merchandise::ProductVariant(v) = line.merchandise() { *v.product().in_any_collection() } else { false } }) .map(|line| *line.quantity()) .sum(); if qualifying_qty < 5 { return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] }); } // Apply 10% off the order subtotal Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![schema::CartOperation::OrderDiscountsAdd( schema::OrderDiscountsAddOperation { selection_strategy: schema::OrderDiscountSelectionStrategy::First, candidates: vec![schema::OrderDiscountCandidate { targets: vec![schema::OrderDiscountCandidateTarget::OrderSubtotal( schema::OrderSubtotalTarget { excluded_cart_line_ids: vec![], }, )], message: Some("Volume discount: 10% off".to_string()), value: schema::OrderDiscountCandidateValue::Percentage( schema::Percentage { value: Decimal(10.0) } ), conditions: None, associated_discount_code: None, }], }, )], }) }
Step 1.4: Test, deploy, activate
# Local development with hot reload shopify app dev # When ready, deploy shopify app deploy # In the GraphiQL panel that opens (press `g` in the dev terminal), # create the automatic discount that uses your Function:
mutation { discountAutomaticAppCreate( automaticAppDiscount: { title: "Volume Discount (5+ collection items)" functionHandle: "volume-discount-fn" discountClasses: [ORDER] startsAt: "2026-04-16T00:00:00Z" } ) { automaticAppDiscount { discountId } userErrors { field message } } }
That's it. The Function is live, version-controlled, and replaces the old Script entirely.
Tutorial 2: Replacing a Shipping Script (Hide Method Above Cart Threshold)
A common Script: "Hide Express Shipping when cart subtotal is over $500 to prevent expensive overnight on big orders." Here's the Delivery Customization Function version.
Step 2.1: Scaffold
shopify app generate extension --template delivery_customization --name hide-express-fn
Step 2.2: Input query (src/run.graphql)
query Input { cart { cost { subtotalAmount { amount } } deliveryGroups { deliveryOptions { handle title } } } }
Step 2.3: Logic (src/run.js — JavaScript variant)
// @ts-check /** * @typedef {import("../generated/api").RunInput} RunInput * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult */ const NO_CHANGES = { operations: [] }; const THRESHOLD = 500.0; const HIDE_TITLES = ["Express", "Overnight"]; /** * @param {RunInput} input * @returns {FunctionRunResult} */ export function run(input) { const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount); if (subtotal < THRESHOLD) return NO_CHANGES; const operations = input.cart.deliveryGroups.flatMap((group) => group.deliveryOptions .filter((opt) => HIDE_TITLES.some((t) => opt.title.includes(t))) .map((opt) => ({ hide: { deliveryOptionHandle: opt.handle }, })) ); return { operations }; }
Step 2.4: Activate via Admin (no GraphQL needed)
Delivery Customizations have a built-in Admin UI. After shopify app deploy:
Go to Settings → Shipping and delivery
Scroll to Customizations section at the bottom
Click Add customization → select your Function
Save
The hide rule is live in production. No mutation required.

Tutorial 3: Replacing a Payment Script (Hide COD for B2B)
Old Script: "Hide Cash on Delivery for any customer tagged 'B2B'." Here's the Payment Customization version.
Step 3.1: Scaffold
shopify app generate extension --template payment_customization --name hide-cod-b2b-fn
Step 3.2: Input query
query Input { cart { buyerIdentity { customer { hasTags(tags: [{ tag: "B2B" }]) { tag hasTag } } } } paymentMethods { id name } }
Step 3.3: Logic (src/run.js)
const NO_CHANGES = { operations: [] }; export function run(input) { const tagCheck = input.cart?.buyerIdentity?.customer?.hasTags?.[0]; const isB2B = tagCheck?.hasTag === true; if (!isB2B) return NO_CHANGES; const codMethod = input.paymentMethods.find((pm) => pm.name.toLowerCase().includes("cash on delivery") ); if (!codMethod) return NO_CHANGES; return { operations: [{ hide: { paymentMethodId: codMethod.id } }], }; }
Step 3.4: Activate
Payment Customizations also have an Admin UI under Settings → Payments → Customizations. Same flow as delivery — pick your Function, save, done.
Testing Strategy: The Tagged Customer Pattern
Functions don't have a "draft mode" you can toggle in admin. The professional pattern is to gate the new Function on a customer tag, run both the old Script and the new Function in parallel, verify they produce identical output for tagged users, then flip the switch.
Step 1: Tag your test users
In Customers, add the tag FN-TESTER to two or three internal accounts.
Step 2: Branch the Function on tag presence
// At the top of your run function let is_tester = input .cart() .buyer_identity() .and_then(|bi| bi.customer()) .map(|c| c.has_any_tag()) .unwrap_or(&false); if !*is_tester { return Ok(default_result); // Fall through to existing Script } // New Function logic only runs for tagged users
Step 3: Add hasAnyTag to your input query
cart { buyerIdentity { customer { hasAnyTag(tags: ["FN-TESTER"]) } } }
Step 4: Verify in checkout
Log in as a tagged user, walk through checkout, confirm the Function fires. Log in as an untagged user, confirm the old Script still runs. When parity holds for a few days, remove the tag check and let the Function run for everyone.
Step 5: Unpublish the old Script
Go to Apps → Script Editor → [Your Script] → Unpublish. Once unpublished, the Function is the sole source of truth.
Deployment Workflow That Actually Scales
Don't deploy from a developer's laptop forever. Once you've migrated one or two Scripts, set up a real CI pipeline.
The minimum viable workflow
# .github/workflows/deploy-functions.yml name: Deploy Shopify Functions on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - uses: dtolnay/rust-toolchain@stable with: { targets: wasm32-wasip1 } - run: npm install -g @shopify/cli@latest - run: shopify app deploy --force env: SHOPIFY_CLI_PARTNERS_TOKEN: ${{ secrets.SHOPIFY_CLI_PARTNERS_TOKEN }}
Generate the partner token from your Partner dashboard under Settings → Tokens. Now every merge to main ships your Functions. No more "did Mike deploy that?" Slack threads.
Versioning and rollback
shopify app deploy creates a versioned snapshot. To roll back:
shopify app versions list shopify app release --version <previous-version-id
Compared to Scripts — where rollback meant "remember the old code and paste it back in" — this is night and day.

What Most Teams Get Wrong
After helping Plus merchants through this migration over the past year, the same five mistakes show up.
1. Treating Functions like a 1:1 Script port. They're not. A single Function can replace three Scripts with cleaner branching. Audit your Scripts as a system before rewriting.
2. Forgetting the read scopes. Many Functions need read_customers, read_orders, or write_discounts. Add them in shopify.app.toml under scopes and re-authorize the app, or your input query returns null.
3. Running Functions on un-tagged customers without parity testing. Even if your code looks right, edge cases (empty cart, gift cards, store credit, B2B drafts) will surface problems. Tag-gated rollout costs you two days and saves you a P1 outage.
4. Skipping the Customizations Report. It's the single best inventory of what you actually have running. Don't migrate from memory — migrate from the report.
5. Hardcoding collection IDs and customer tags. Use Function configuration via metafields if you need merchant-tunable values. The CLI can scaffold metafield-backed config — see Shopify's docs on Function configuration.
Migration Checklist for the Next 75 Days
A realistic week-by-week plan to get to June 30 calmly.
Week | Action |
|---|---|
Week 1 (this week) | Pull the Customizations Report. Inventory every Script. Decide Function vs public app vs delete-it. |
Weeks 2–3 | Set up local dev environment. Scaffold the first Function. Migrate the simplest Script (usually a payment hide rule). |
Weeks 4–6 | Migrate discount Scripts. These take longest because the Discounts API is the richest. Tag-test thoroughly. |
Weeks 7–8 | Migrate shipping/delivery Scripts. Activate Delivery Customizations via Admin. |
Weeks 9–10 | Set up CI pipeline. Move all deployments off developer laptops. |
Weeks 11 (mid-June) | Final parity verification. Unpublish all Scripts. Run the store on Functions only for two weeks. |
June 30 | Sunset day arrives. Nothing breaks because you finished early. |
If you start this week, you have buffer. If you start in June, you don't.

Frequently Asked Questions
Do I need Shopify Plus to use Functions?
Custom Functions require Shopify Plus, but public-app Functions work on every plan. If you're not on Plus, you have two paths: install a public app from the Shopify App Store that ships the Function for you, or upgrade to Plus to write your own custom Functions. Most large merchants who had Scripts already had Plus, so this rarely changes anything practically.
Can I write Functions in TypeScript?
Yes — TypeScript is fully supported and the CLI scaffolds it for you. When you run shopify app generate extension and choose "JavaScript," the generated project includes type declarations from import("../generated/api"). You can convert files to .ts and add a tsconfig.json if you prefer. The compiled output (WASM) is identical regardless of source language.
How fast are Functions compared to Scripts?
Functions execute in under 5ms typically — significantly faster than Ruby Scripts. Because they're compiled to WebAssembly and run in a tight runtime, Shopify enforces a 5ms execution budget. If your Function exceeds it, the operation is dropped and your Function returns no operations. In practice, a well-written Function uses 1–2ms. The performance ceiling is much higher than Scripts.
Can a Function call an external API?
No — Functions cannot make network requests. They are pure computation: input cart data → output operations. If you need external data (a CRM lookup, a real-time inventory check), you'll need to either store the data in metafields ahead of time, or use a different surface (App Proxy, webhooks, Cart Transform with backend lookup). This is the most common reason teams need to redesign rather than port.
What's the difference between Cart Transform and Discounts?
Discounts apply price changes; Cart Transform changes the cart's contents. Use the Discounts API to apply a 10% off, free shipping, or BOGO. Use Cart Transform to bundle two products into one line item, or split one variant into multiple. Many old Scripts mixed both concerns — when migrating, separate them into two Functions.
How do I test a Function locally without a dev store?
You can run unit tests with cargo test (Rust) or npm test (JS), but full integration testing requires a dev store. The CLI provides shopify app function run which executes your Function against a sample input file — useful for fast iteration. But to confirm checkout behavior end-to-end, you need a real store with a real cart.
Can I have multiple Functions of the same type?
Yes — Shopify supports multiple Functions per target, and they execute in deterministic order. For discounts, the order is governed by Shopify's discount stacking rules. For Delivery and Payment Customizations, you can chain Functions but each output feeds the next. Most teams use one Function per type for simplicity.
What happens to my Script after I deploy the Function?
Both run in parallel until you unpublish the Script in Apps → Script Editor. This is intentional — it gives you the parallel-run testing window. After verifying the Function works, manually unpublish the Script. After June 30, 2026, all Scripts stop executing whether you unpublished them or not.
Will the migration affect my SEO or theme?
No — Functions execute server-side at checkout and never touch your theme or product pages. They only modify discounts, shipping options, and payment methods at checkout. Your storefront, product templates, and SEO are completely untouched.
How do I migrate a Script that uses Input.line_items with custom properties?
Custom properties are accessible via the attribute field on cart lines in the GraphQL input. Add attribute(key: "your-key") { value } inside the lines selection. The Function reads it identically to how Scripts read line item properties — just through GraphQL instead of Ruby method calls.
What about analytics and order tags? Can Functions write data?
Functions cannot write order tags or trigger webhooks themselves — they only return operations on the current cart. For tagging or downstream workflows, use Shopify Flow triggered by the order creation event. Many merchants combine a Function (for the discount) with a Flow (for tagging "VOLUME-DISCOUNT-APPLIED" on the order).
Is there a public app I can install instead of building a custom Function?
Yes — the Shopify App Store has dozens of apps that wrap Functions for common use cases. Search for "discount function," "delivery customization," or "payment customization." For straightforward use cases (volume discounts, hide payment methods by tag, free shipping over X), an existing app may save you days of dev work. Reserve custom Functions for logic that's truly unique to your business.
What if I miss the June 30 deadline?
The Script stops executing — there is no fallback, no grace period, and no extension. Whatever the Script was doing (the discount, the hidden shipping rate, the blocked payment method) reverts to default behavior at midnight UTC on July 1. If your business depends on that logic, plan to be live well before that date. The migration takes longer than most teams estimate, especially with parity testing.
Can I delete the Script Editor app entirely?
You can uninstall it after June 30, 2026, but Shopify will likely auto-remove it. Once Scripts stop executing, the editor serves no purpose. You can also unpublish all Scripts now and uninstall the app immediately if you've completed migration — your Functions are independent.
What to Do This Week
Don't read this article and close the tab. Do these four things in the next seven days.
1. Pull the Customizations Report. Go to Settings → Checkout → Customizations Report. Export it. This is your migration backlog.
2. Set up your dev environment. Install Node 18+, Shopify CLI, and Rust (if applicable). Confirm shopify version works. Total time: 30 minutes.
3. Scaffold and ship one tiny Function to a dev store. Pick the simplest Script you have — usually a payment hide rule. Migrate it end to end. Even if it never goes to production, you've validated the toolchain.
4. Block calendar time for the next eight weeks. Migrations don't happen in spare moments between sprints. Carve out a recurring slot — every Tuesday and Thursday afternoon, for example — and treat it like a release.
From the teams we've seen migrate, the pattern is consistent: two weeks of stalling, three weeks of actual work, one week of cleanup. That's six weeks. You have ten. The slack is there — burn it on code review and QA, not on putting off the start.
And once your checkout logic is sorted, the next thing most teams tackle is post-purchase — customer address changes, swaps, discount additions after the order is placed. If that's on your roadmap (and it should be, whether you're a high-volume Plus operator or a CX-led Advanced store), Revize is on the Shopify App Store and works alongside every Function you'll build here.
Resources
Related Articles
It's April 16, 2026. Yesterday — April 15 — was the day Shopify locked the Script Editor for good. You can no longer create or publish a new Script. The execution shutdown lands in 75 days, on June 30, 2026.
If you're a Shopify Plus developer — or the agency running a Plus store — who pushed this migration to "next sprint" for the last twelve months, you have a problem. Not a "nice to fix" problem. A "your checkout breaks at midnight on July 1" problem. Most Plus stores have accumulated 5 to 20 Scripts over the years, each quietly powering a discount rule, a shipping hide, or a payment gate that nobody remembers writing.
This guide is the technical migration manual we wished existed in January. It covers the actual code — not just the strategy. By the time you finish reading, you'll know how to scaffold a Function with the Shopify CLI, write the Rust or JavaScript logic for discounts, delivery customizations, and payment customizations, test it safely against a tagged subset of your customers, and ship it to production without breaking your existing checkout.
Let's get the Scripts off your store and the Functions on it.

Quick Answer: Scripts → Functions in 60 Seconds
The migration in one paragraph: Shopify Scripts (Ruby code in the Script Editor, Plus-only) are being replaced by Shopify Functions (WebAssembly modules written in Rust or JavaScript, available to all plans). You scaffold a Function with
shopify app generate extension, write arun.graphqlquery that pulls the cart data you need, write arun.rsorrun.jsfile that returns operations (discounts, hidden shipping methods, etc.), then deploy withshopify app deployand activate it via Admin or a GraphQL mutation. Functions execute as compiled WASM at sub-5ms latency, run on every plan, and are the only customization path Shopify supports going forward.
What's Actually Changing on June 30
Before we touch any code, get the dates straight. There are two of them, and both matter.
Date | What Happens | Your Action |
|---|---|---|
April 15, 2026 (passed) | Script Editor read-only. No new Scripts. No edits to existing Scripts. | Existing Scripts still execute. Migrate now or freeze your logic. |
June 30, 2026 | All Shopify Scripts stop executing. Period. | Your Function replacement must be live before this date. |
The migration is binary. Either your Function is deployed by June 30 and your checkout keeps working, or it isn't — and every affected cart quietly defaults back to standard pricing, standard shipping rates, and every payment method enabled. There's no partial credit. The Script either runs or it doesn't, and after June 30 it doesn't.
Tip: Open
Settings → Checkout → Customizations Reportin your Shopify admin. It lists every active Script in your store, what it does, and the recommended Function replacement type. Start there.
Functions vs Scripts: What Actually Changed
Dimension | Shopify Scripts (deprecated) | Shopify Functions (replacement) |
|---|---|---|
Language | Ruby DSL (Shopify-specific) | Rust, JavaScript, TypeScript |
Runtime | Sandboxed Ruby on Shopify infra | WebAssembly (WASM) — sub-5ms execution |
Plan availability | Plus only | All plans (custom apps require Plus; public apps are open) |
Editor | In-admin Script Editor | Local IDE + Shopify CLI |
Versioning | None — live edits | Git-friendly — full version control |
Testing | Manual in checkout | Local dev with |
Deployment | Click "Save" in admin |
|
Targets | Line items, shipping, payments | Discounts, Cart Transform, Validation, Delivery Customization, Payment Customization, Order Routing, Fulfillment Constraints, more |
The architectural shift matters. Scripts were "tweak Ruby in a textarea." Functions are "write a real app, version-control it, test it locally, deploy it through a real CI pipeline." That's a steeper on-ramp. It's also the last migration you'll do for checkout logic in the foreseeable future — Functions are Shopify's long-term commitment, not a stopgap like Scripts turned out to be.
Mapping Your Scripts to the Right Function Type
Every Script you have today maps to exactly one Function API. Here's the lookup table you need pinned to your monitor.
Old Script Type | What It Did | New Function API | Function Target |
|---|---|---|---|
Line Item Script | Apply discounts to specific products / customers / cart conditions | Cart & Checkout Discounts API |
|
Shipping Script (discount) | Free / discounted shipping based on cart rules | Cart & Checkout Discounts API |
|
Shipping Script (hide / rename / reorder) | Hide a shipping rate above $X, rename "Standard" to "Free over $50" | Delivery Customization API |
|
Payment Script | Hide PayPal for B2B, hide COD over $500, reorder methods | Payment Customization API |
|
Cart-modifying Script (rare) | Bundle products, swap line items | Cart Transform API |
|
Block-checkout Script | Reject cart if SKU mix invalid | Cart & Checkout Validation API |
|
If you have ten Scripts, you'll likely build three to five Functions — multiple Scripts often collapse into one Function with cleaner branching logic.

Prerequisite: Set Up Your Local Dev Environment
Before scaffolding any Function, you need three things installed locally. Run these checks in your terminal.
1. Node.js 18+
node --version # Must be >= 18.0.0
If older, install via nvm or download from nodejs.org.
2. Shopify CLI 3+
npm install -g @shopify/cli@latest shopify version # Should output 3.x or higher
3. Rust toolchain (only if you'll write Functions in Rust)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup target add wasm32-wasip1 cargo --version
JavaScript Functions don't need Rust. Pick one language for your team and stick with it — mixing both adds maintenance overhead.
4. A development store
Either log into your Partner dashboard and create a new development store, or use an existing one. You'll deploy Functions to this store before promoting to production.
Scaffolding Your First Function
The CLI does most of the boilerplate. Inside any directory:
# Create a new Shopify app (skip if you already have one) shopify app init my-checkout-functions cd my-checkout-functions # Generate a Function extension shopify app generate extension
The CLI walks you through prompts. For a discount Function, you'd pick:
Type: Function
Template:
discount(orcart_checkout_validation,delivery_customization,payment_customization, etc.)Language: Rust or JavaScript
Name: something like
volume-discount-fn
This creates extensions/volume-discount-fn/ with:
extensions/volume-discount-fn/ ├── shopify.extension.toml # Function config — targets, build, version ├── src/ │ ├── cart_lines_discounts_generate_run.graphql # Input query │ └── cart_lines_discounts_generate_run.rs # Function logic ├── Cargo.toml # Rust dependencies (Rust only) └── README.md
The three files you'll edit constantly are the .toml (config), the .graphql (input), and the .rs / .js (logic). That's it.
Tutorial 1: Replacing a Line Item Script (Volume Discount)
Say your old Script gave 10% off the order subtotal whenever the cart had 5+ units of a specific collection. Here's the Function equivalent.
Step 1.1: The configuration (shopify.extension.toml)
api_version = "2026-01" [[extensions]] name = "volume-discount-fn" handle = "volume-discount-fn" type = "function" [[extensions.targeting]] target = "cart.lines.discounts.generate.run" input_query = "src/cart_lines_discounts_generate_run.graphql" export = "cart_lines_discounts_generate_run" [extensions.build] command = "cargo build --target=wasm32-wasip1 --release" path = "target/wasm32-wasip1/release/volume-discount-fn.wasm" watch = ["src/**/*.rs"]
Step 1.2: The input query (src/cart_lines_discounts_generate_run.graphql)
query Input { cart { lines { id quantity cost { subtotalAmount { amount } } merchandise { ... on ProductVariant { product { inAnyCollection(ids: ["gid://shopify/Collection/123456789"]) } } } } } discount { discountClasses } }
Tip: Functions only see the data you query. Keep the GraphQL minimal — every field you skip means a faster, cheaper execution.
Step 1.3: The logic (src/cart_lines_discounts_generate_run.rs)
use super::schema; use shopify_function::prelude::*; use shopify_function::Result; #[shopify_function] fn cart_lines_discounts_generate_run( input: schema::cart_lines_discounts_generate_run::Input, ) -> Result<schema::CartLinesDiscountsGenerateRunResult> { // Bail if discount class doesn't match let has_order_discount = input .discount() .discount_classes() .contains(&schema::DiscountClass::Order); if !has_order_discount { return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] }); } // Sum quantities of items in the target collection let qualifying_qty: i64 = input .cart() .lines() .iter() .filter(|line| { if let schema::Merchandise::ProductVariant(v) = line.merchandise() { *v.product().in_any_collection() } else { false } }) .map(|line| *line.quantity()) .sum(); if qualifying_qty < 5 { return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] }); } // Apply 10% off the order subtotal Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![schema::CartOperation::OrderDiscountsAdd( schema::OrderDiscountsAddOperation { selection_strategy: schema::OrderDiscountSelectionStrategy::First, candidates: vec![schema::OrderDiscountCandidate { targets: vec![schema::OrderDiscountCandidateTarget::OrderSubtotal( schema::OrderSubtotalTarget { excluded_cart_line_ids: vec![], }, )], message: Some("Volume discount: 10% off".to_string()), value: schema::OrderDiscountCandidateValue::Percentage( schema::Percentage { value: Decimal(10.0) } ), conditions: None, associated_discount_code: None, }], }, )], }) }
Step 1.4: Test, deploy, activate
# Local development with hot reload shopify app dev # When ready, deploy shopify app deploy # In the GraphiQL panel that opens (press `g` in the dev terminal), # create the automatic discount that uses your Function:
mutation { discountAutomaticAppCreate( automaticAppDiscount: { title: "Volume Discount (5+ collection items)" functionHandle: "volume-discount-fn" discountClasses: [ORDER] startsAt: "2026-04-16T00:00:00Z" } ) { automaticAppDiscount { discountId } userErrors { field message } } }
That's it. The Function is live, version-controlled, and replaces the old Script entirely.
Tutorial 2: Replacing a Shipping Script (Hide Method Above Cart Threshold)
A common Script: "Hide Express Shipping when cart subtotal is over $500 to prevent expensive overnight on big orders." Here's the Delivery Customization Function version.
Step 2.1: Scaffold
shopify app generate extension --template delivery_customization --name hide-express-fn
Step 2.2: Input query (src/run.graphql)
query Input { cart { cost { subtotalAmount { amount } } deliveryGroups { deliveryOptions { handle title } } } }
Step 2.3: Logic (src/run.js — JavaScript variant)
// @ts-check /** * @typedef {import("../generated/api").RunInput} RunInput * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult */ const NO_CHANGES = { operations: [] }; const THRESHOLD = 500.0; const HIDE_TITLES = ["Express", "Overnight"]; /** * @param {RunInput} input * @returns {FunctionRunResult} */ export function run(input) { const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount); if (subtotal < THRESHOLD) return NO_CHANGES; const operations = input.cart.deliveryGroups.flatMap((group) => group.deliveryOptions .filter((opt) => HIDE_TITLES.some((t) => opt.title.includes(t))) .map((opt) => ({ hide: { deliveryOptionHandle: opt.handle }, })) ); return { operations }; }
Step 2.4: Activate via Admin (no GraphQL needed)
Delivery Customizations have a built-in Admin UI. After shopify app deploy:
Go to Settings → Shipping and delivery
Scroll to Customizations section at the bottom
Click Add customization → select your Function
Save
The hide rule is live in production. No mutation required.

Tutorial 3: Replacing a Payment Script (Hide COD for B2B)
Old Script: "Hide Cash on Delivery for any customer tagged 'B2B'." Here's the Payment Customization version.
Step 3.1: Scaffold
shopify app generate extension --template payment_customization --name hide-cod-b2b-fn
Step 3.2: Input query
query Input { cart { buyerIdentity { customer { hasTags(tags: [{ tag: "B2B" }]) { tag hasTag } } } } paymentMethods { id name } }
Step 3.3: Logic (src/run.js)
const NO_CHANGES = { operations: [] }; export function run(input) { const tagCheck = input.cart?.buyerIdentity?.customer?.hasTags?.[0]; const isB2B = tagCheck?.hasTag === true; if (!isB2B) return NO_CHANGES; const codMethod = input.paymentMethods.find((pm) => pm.name.toLowerCase().includes("cash on delivery") ); if (!codMethod) return NO_CHANGES; return { operations: [{ hide: { paymentMethodId: codMethod.id } }], }; }
Step 3.4: Activate
Payment Customizations also have an Admin UI under Settings → Payments → Customizations. Same flow as delivery — pick your Function, save, done.
Testing Strategy: The Tagged Customer Pattern
Functions don't have a "draft mode" you can toggle in admin. The professional pattern is to gate the new Function on a customer tag, run both the old Script and the new Function in parallel, verify they produce identical output for tagged users, then flip the switch.
Step 1: Tag your test users
In Customers, add the tag FN-TESTER to two or three internal accounts.
Step 2: Branch the Function on tag presence
// At the top of your run function let is_tester = input .cart() .buyer_identity() .and_then(|bi| bi.customer()) .map(|c| c.has_any_tag()) .unwrap_or(&false); if !*is_tester { return Ok(default_result); // Fall through to existing Script } // New Function logic only runs for tagged users
Step 3: Add hasAnyTag to your input query
cart { buyerIdentity { customer { hasAnyTag(tags: ["FN-TESTER"]) } } }
Step 4: Verify in checkout
Log in as a tagged user, walk through checkout, confirm the Function fires. Log in as an untagged user, confirm the old Script still runs. When parity holds for a few days, remove the tag check and let the Function run for everyone.
Step 5: Unpublish the old Script
Go to Apps → Script Editor → [Your Script] → Unpublish. Once unpublished, the Function is the sole source of truth.
Deployment Workflow That Actually Scales
Don't deploy from a developer's laptop forever. Once you've migrated one or two Scripts, set up a real CI pipeline.
The minimum viable workflow
# .github/workflows/deploy-functions.yml name: Deploy Shopify Functions on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - uses: dtolnay/rust-toolchain@stable with: { targets: wasm32-wasip1 } - run: npm install -g @shopify/cli@latest - run: shopify app deploy --force env: SHOPIFY_CLI_PARTNERS_TOKEN: ${{ secrets.SHOPIFY_CLI_PARTNERS_TOKEN }}
Generate the partner token from your Partner dashboard under Settings → Tokens. Now every merge to main ships your Functions. No more "did Mike deploy that?" Slack threads.
Versioning and rollback
shopify app deploy creates a versioned snapshot. To roll back:
shopify app versions list shopify app release --version <previous-version-id
Compared to Scripts — where rollback meant "remember the old code and paste it back in" — this is night and day.

What Most Teams Get Wrong
After helping Plus merchants through this migration over the past year, the same five mistakes show up.
1. Treating Functions like a 1:1 Script port. They're not. A single Function can replace three Scripts with cleaner branching. Audit your Scripts as a system before rewriting.
2. Forgetting the read scopes. Many Functions need read_customers, read_orders, or write_discounts. Add them in shopify.app.toml under scopes and re-authorize the app, or your input query returns null.
3. Running Functions on un-tagged customers without parity testing. Even if your code looks right, edge cases (empty cart, gift cards, store credit, B2B drafts) will surface problems. Tag-gated rollout costs you two days and saves you a P1 outage.
4. Skipping the Customizations Report. It's the single best inventory of what you actually have running. Don't migrate from memory — migrate from the report.
5. Hardcoding collection IDs and customer tags. Use Function configuration via metafields if you need merchant-tunable values. The CLI can scaffold metafield-backed config — see Shopify's docs on Function configuration.
Migration Checklist for the Next 75 Days
A realistic week-by-week plan to get to June 30 calmly.
Week | Action |
|---|---|
Week 1 (this week) | Pull the Customizations Report. Inventory every Script. Decide Function vs public app vs delete-it. |
Weeks 2–3 | Set up local dev environment. Scaffold the first Function. Migrate the simplest Script (usually a payment hide rule). |
Weeks 4–6 | Migrate discount Scripts. These take longest because the Discounts API is the richest. Tag-test thoroughly. |
Weeks 7–8 | Migrate shipping/delivery Scripts. Activate Delivery Customizations via Admin. |
Weeks 9–10 | Set up CI pipeline. Move all deployments off developer laptops. |
Weeks 11 (mid-June) | Final parity verification. Unpublish all Scripts. Run the store on Functions only for two weeks. |
June 30 | Sunset day arrives. Nothing breaks because you finished early. |
If you start this week, you have buffer. If you start in June, you don't.

Frequently Asked Questions
Do I need Shopify Plus to use Functions?
Custom Functions require Shopify Plus, but public-app Functions work on every plan. If you're not on Plus, you have two paths: install a public app from the Shopify App Store that ships the Function for you, or upgrade to Plus to write your own custom Functions. Most large merchants who had Scripts already had Plus, so this rarely changes anything practically.
Can I write Functions in TypeScript?
Yes — TypeScript is fully supported and the CLI scaffolds it for you. When you run shopify app generate extension and choose "JavaScript," the generated project includes type declarations from import("../generated/api"). You can convert files to .ts and add a tsconfig.json if you prefer. The compiled output (WASM) is identical regardless of source language.
How fast are Functions compared to Scripts?
Functions execute in under 5ms typically — significantly faster than Ruby Scripts. Because they're compiled to WebAssembly and run in a tight runtime, Shopify enforces a 5ms execution budget. If your Function exceeds it, the operation is dropped and your Function returns no operations. In practice, a well-written Function uses 1–2ms. The performance ceiling is much higher than Scripts.
Can a Function call an external API?
No — Functions cannot make network requests. They are pure computation: input cart data → output operations. If you need external data (a CRM lookup, a real-time inventory check), you'll need to either store the data in metafields ahead of time, or use a different surface (App Proxy, webhooks, Cart Transform with backend lookup). This is the most common reason teams need to redesign rather than port.
What's the difference between Cart Transform and Discounts?
Discounts apply price changes; Cart Transform changes the cart's contents. Use the Discounts API to apply a 10% off, free shipping, or BOGO. Use Cart Transform to bundle two products into one line item, or split one variant into multiple. Many old Scripts mixed both concerns — when migrating, separate them into two Functions.
How do I test a Function locally without a dev store?
You can run unit tests with cargo test (Rust) or npm test (JS), but full integration testing requires a dev store. The CLI provides shopify app function run which executes your Function against a sample input file — useful for fast iteration. But to confirm checkout behavior end-to-end, you need a real store with a real cart.
Can I have multiple Functions of the same type?
Yes — Shopify supports multiple Functions per target, and they execute in deterministic order. For discounts, the order is governed by Shopify's discount stacking rules. For Delivery and Payment Customizations, you can chain Functions but each output feeds the next. Most teams use one Function per type for simplicity.
What happens to my Script after I deploy the Function?
Both run in parallel until you unpublish the Script in Apps → Script Editor. This is intentional — it gives you the parallel-run testing window. After verifying the Function works, manually unpublish the Script. After June 30, 2026, all Scripts stop executing whether you unpublished them or not.
Will the migration affect my SEO or theme?
No — Functions execute server-side at checkout and never touch your theme or product pages. They only modify discounts, shipping options, and payment methods at checkout. Your storefront, product templates, and SEO are completely untouched.
How do I migrate a Script that uses Input.line_items with custom properties?
Custom properties are accessible via the attribute field on cart lines in the GraphQL input. Add attribute(key: "your-key") { value } inside the lines selection. The Function reads it identically to how Scripts read line item properties — just through GraphQL instead of Ruby method calls.
What about analytics and order tags? Can Functions write data?
Functions cannot write order tags or trigger webhooks themselves — they only return operations on the current cart. For tagging or downstream workflows, use Shopify Flow triggered by the order creation event. Many merchants combine a Function (for the discount) with a Flow (for tagging "VOLUME-DISCOUNT-APPLIED" on the order).
Is there a public app I can install instead of building a custom Function?
Yes — the Shopify App Store has dozens of apps that wrap Functions for common use cases. Search for "discount function," "delivery customization," or "payment customization." For straightforward use cases (volume discounts, hide payment methods by tag, free shipping over X), an existing app may save you days of dev work. Reserve custom Functions for logic that's truly unique to your business.
What if I miss the June 30 deadline?
The Script stops executing — there is no fallback, no grace period, and no extension. Whatever the Script was doing (the discount, the hidden shipping rate, the blocked payment method) reverts to default behavior at midnight UTC on July 1. If your business depends on that logic, plan to be live well before that date. The migration takes longer than most teams estimate, especially with parity testing.
Can I delete the Script Editor app entirely?
You can uninstall it after June 30, 2026, but Shopify will likely auto-remove it. Once Scripts stop executing, the editor serves no purpose. You can also unpublish all Scripts now and uninstall the app immediately if you've completed migration — your Functions are independent.
What to Do This Week
Don't read this article and close the tab. Do these four things in the next seven days.
1. Pull the Customizations Report. Go to Settings → Checkout → Customizations Report. Export it. This is your migration backlog.
2. Set up your dev environment. Install Node 18+, Shopify CLI, and Rust (if applicable). Confirm shopify version works. Total time: 30 minutes.
3. Scaffold and ship one tiny Function to a dev store. Pick the simplest Script you have — usually a payment hide rule. Migrate it end to end. Even if it never goes to production, you've validated the toolchain.
4. Block calendar time for the next eight weeks. Migrations don't happen in spare moments between sprints. Carve out a recurring slot — every Tuesday and Thursday afternoon, for example — and treat it like a release.
From the teams we've seen migrate, the pattern is consistent: two weeks of stalling, three weeks of actual work, one week of cleanup. That's six weeks. You have ten. The slack is there — burn it on code review and QA, not on putting off the start.
And once your checkout logic is sorted, the next thing most teams tackle is post-purchase — customer address changes, swaps, discount additions after the order is placed. If that's on your roadmap (and it should be, whether you're a high-volume Plus operator or a CX-led Advanced store), Revize is on the Shopify App Store and works alongside every Function you'll build here.
Resources
Related Articles
It's April 16, 2026. Yesterday — April 15 — was the day Shopify locked the Script Editor for good. You can no longer create or publish a new Script. The execution shutdown lands in 75 days, on June 30, 2026.
If you're a Shopify Plus developer — or the agency running a Plus store — who pushed this migration to "next sprint" for the last twelve months, you have a problem. Not a "nice to fix" problem. A "your checkout breaks at midnight on July 1" problem. Most Plus stores have accumulated 5 to 20 Scripts over the years, each quietly powering a discount rule, a shipping hide, or a payment gate that nobody remembers writing.
This guide is the technical migration manual we wished existed in January. It covers the actual code — not just the strategy. By the time you finish reading, you'll know how to scaffold a Function with the Shopify CLI, write the Rust or JavaScript logic for discounts, delivery customizations, and payment customizations, test it safely against a tagged subset of your customers, and ship it to production without breaking your existing checkout.
Let's get the Scripts off your store and the Functions on it.

Quick Answer: Scripts → Functions in 60 Seconds
The migration in one paragraph: Shopify Scripts (Ruby code in the Script Editor, Plus-only) are being replaced by Shopify Functions (WebAssembly modules written in Rust or JavaScript, available to all plans). You scaffold a Function with
shopify app generate extension, write arun.graphqlquery that pulls the cart data you need, write arun.rsorrun.jsfile that returns operations (discounts, hidden shipping methods, etc.), then deploy withshopify app deployand activate it via Admin or a GraphQL mutation. Functions execute as compiled WASM at sub-5ms latency, run on every plan, and are the only customization path Shopify supports going forward.
What's Actually Changing on June 30
Before we touch any code, get the dates straight. There are two of them, and both matter.
Date | What Happens | Your Action |
|---|---|---|
April 15, 2026 (passed) | Script Editor read-only. No new Scripts. No edits to existing Scripts. | Existing Scripts still execute. Migrate now or freeze your logic. |
June 30, 2026 | All Shopify Scripts stop executing. Period. | Your Function replacement must be live before this date. |
The migration is binary. Either your Function is deployed by June 30 and your checkout keeps working, or it isn't — and every affected cart quietly defaults back to standard pricing, standard shipping rates, and every payment method enabled. There's no partial credit. The Script either runs or it doesn't, and after June 30 it doesn't.
Tip: Open
Settings → Checkout → Customizations Reportin your Shopify admin. It lists every active Script in your store, what it does, and the recommended Function replacement type. Start there.
Functions vs Scripts: What Actually Changed
Dimension | Shopify Scripts (deprecated) | Shopify Functions (replacement) |
|---|---|---|
Language | Ruby DSL (Shopify-specific) | Rust, JavaScript, TypeScript |
Runtime | Sandboxed Ruby on Shopify infra | WebAssembly (WASM) — sub-5ms execution |
Plan availability | Plus only | All plans (custom apps require Plus; public apps are open) |
Editor | In-admin Script Editor | Local IDE + Shopify CLI |
Versioning | None — live edits | Git-friendly — full version control |
Testing | Manual in checkout | Local dev with |
Deployment | Click "Save" in admin |
|
Targets | Line items, shipping, payments | Discounts, Cart Transform, Validation, Delivery Customization, Payment Customization, Order Routing, Fulfillment Constraints, more |
The architectural shift matters. Scripts were "tweak Ruby in a textarea." Functions are "write a real app, version-control it, test it locally, deploy it through a real CI pipeline." That's a steeper on-ramp. It's also the last migration you'll do for checkout logic in the foreseeable future — Functions are Shopify's long-term commitment, not a stopgap like Scripts turned out to be.
Mapping Your Scripts to the Right Function Type
Every Script you have today maps to exactly one Function API. Here's the lookup table you need pinned to your monitor.
Old Script Type | What It Did | New Function API | Function Target |
|---|---|---|---|
Line Item Script | Apply discounts to specific products / customers / cart conditions | Cart & Checkout Discounts API |
|
Shipping Script (discount) | Free / discounted shipping based on cart rules | Cart & Checkout Discounts API |
|
Shipping Script (hide / rename / reorder) | Hide a shipping rate above $X, rename "Standard" to "Free over $50" | Delivery Customization API |
|
Payment Script | Hide PayPal for B2B, hide COD over $500, reorder methods | Payment Customization API |
|
Cart-modifying Script (rare) | Bundle products, swap line items | Cart Transform API |
|
Block-checkout Script | Reject cart if SKU mix invalid | Cart & Checkout Validation API |
|
If you have ten Scripts, you'll likely build three to five Functions — multiple Scripts often collapse into one Function with cleaner branching logic.

Prerequisite: Set Up Your Local Dev Environment
Before scaffolding any Function, you need three things installed locally. Run these checks in your terminal.
1. Node.js 18+
node --version # Must be >= 18.0.0
If older, install via nvm or download from nodejs.org.
2. Shopify CLI 3+
npm install -g @shopify/cli@latest shopify version # Should output 3.x or higher
3. Rust toolchain (only if you'll write Functions in Rust)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh rustup target add wasm32-wasip1 cargo --version
JavaScript Functions don't need Rust. Pick one language for your team and stick with it — mixing both adds maintenance overhead.
4. A development store
Either log into your Partner dashboard and create a new development store, or use an existing one. You'll deploy Functions to this store before promoting to production.
Scaffolding Your First Function
The CLI does most of the boilerplate. Inside any directory:
# Create a new Shopify app (skip if you already have one) shopify app init my-checkout-functions cd my-checkout-functions # Generate a Function extension shopify app generate extension
The CLI walks you through prompts. For a discount Function, you'd pick:
Type: Function
Template:
discount(orcart_checkout_validation,delivery_customization,payment_customization, etc.)Language: Rust or JavaScript
Name: something like
volume-discount-fn
This creates extensions/volume-discount-fn/ with:
extensions/volume-discount-fn/ ├── shopify.extension.toml # Function config — targets, build, version ├── src/ │ ├── cart_lines_discounts_generate_run.graphql # Input query │ └── cart_lines_discounts_generate_run.rs # Function logic ├── Cargo.toml # Rust dependencies (Rust only) └── README.md
The three files you'll edit constantly are the .toml (config), the .graphql (input), and the .rs / .js (logic). That's it.
Tutorial 1: Replacing a Line Item Script (Volume Discount)
Say your old Script gave 10% off the order subtotal whenever the cart had 5+ units of a specific collection. Here's the Function equivalent.
Step 1.1: The configuration (shopify.extension.toml)
api_version = "2026-01" [[extensions]] name = "volume-discount-fn" handle = "volume-discount-fn" type = "function" [[extensions.targeting]] target = "cart.lines.discounts.generate.run" input_query = "src/cart_lines_discounts_generate_run.graphql" export = "cart_lines_discounts_generate_run" [extensions.build] command = "cargo build --target=wasm32-wasip1 --release" path = "target/wasm32-wasip1/release/volume-discount-fn.wasm" watch = ["src/**/*.rs"]
Step 1.2: The input query (src/cart_lines_discounts_generate_run.graphql)
query Input { cart { lines { id quantity cost { subtotalAmount { amount } } merchandise { ... on ProductVariant { product { inAnyCollection(ids: ["gid://shopify/Collection/123456789"]) } } } } } discount { discountClasses } }
Tip: Functions only see the data you query. Keep the GraphQL minimal — every field you skip means a faster, cheaper execution.
Step 1.3: The logic (src/cart_lines_discounts_generate_run.rs)
use super::schema; use shopify_function::prelude::*; use shopify_function::Result; #[shopify_function] fn cart_lines_discounts_generate_run( input: schema::cart_lines_discounts_generate_run::Input, ) -> Result<schema::CartLinesDiscountsGenerateRunResult> { // Bail if discount class doesn't match let has_order_discount = input .discount() .discount_classes() .contains(&schema::DiscountClass::Order); if !has_order_discount { return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] }); } // Sum quantities of items in the target collection let qualifying_qty: i64 = input .cart() .lines() .iter() .filter(|line| { if let schema::Merchandise::ProductVariant(v) = line.merchandise() { *v.product().in_any_collection() } else { false } }) .map(|line| *line.quantity()) .sum(); if qualifying_qty < 5 { return Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![] }); } // Apply 10% off the order subtotal Ok(schema::CartLinesDiscountsGenerateRunResult { operations: vec![schema::CartOperation::OrderDiscountsAdd( schema::OrderDiscountsAddOperation { selection_strategy: schema::OrderDiscountSelectionStrategy::First, candidates: vec![schema::OrderDiscountCandidate { targets: vec![schema::OrderDiscountCandidateTarget::OrderSubtotal( schema::OrderSubtotalTarget { excluded_cart_line_ids: vec![], }, )], message: Some("Volume discount: 10% off".to_string()), value: schema::OrderDiscountCandidateValue::Percentage( schema::Percentage { value: Decimal(10.0) } ), conditions: None, associated_discount_code: None, }], }, )], }) }
Step 1.4: Test, deploy, activate
# Local development with hot reload shopify app dev # When ready, deploy shopify app deploy # In the GraphiQL panel that opens (press `g` in the dev terminal), # create the automatic discount that uses your Function:
mutation { discountAutomaticAppCreate( automaticAppDiscount: { title: "Volume Discount (5+ collection items)" functionHandle: "volume-discount-fn" discountClasses: [ORDER] startsAt: "2026-04-16T00:00:00Z" } ) { automaticAppDiscount { discountId } userErrors { field message } } }
That's it. The Function is live, version-controlled, and replaces the old Script entirely.
Tutorial 2: Replacing a Shipping Script (Hide Method Above Cart Threshold)
A common Script: "Hide Express Shipping when cart subtotal is over $500 to prevent expensive overnight on big orders." Here's the Delivery Customization Function version.
Step 2.1: Scaffold
shopify app generate extension --template delivery_customization --name hide-express-fn
Step 2.2: Input query (src/run.graphql)
query Input { cart { cost { subtotalAmount { amount } } deliveryGroups { deliveryOptions { handle title } } } }
Step 2.3: Logic (src/run.js — JavaScript variant)
// @ts-check /** * @typedef {import("../generated/api").RunInput} RunInput * @typedef {import("../generated/api").FunctionRunResult} FunctionRunResult */ const NO_CHANGES = { operations: [] }; const THRESHOLD = 500.0; const HIDE_TITLES = ["Express", "Overnight"]; /** * @param {RunInput} input * @returns {FunctionRunResult} */ export function run(input) { const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount); if (subtotal < THRESHOLD) return NO_CHANGES; const operations = input.cart.deliveryGroups.flatMap((group) => group.deliveryOptions .filter((opt) => HIDE_TITLES.some((t) => opt.title.includes(t))) .map((opt) => ({ hide: { deliveryOptionHandle: opt.handle }, })) ); return { operations }; }
Step 2.4: Activate via Admin (no GraphQL needed)
Delivery Customizations have a built-in Admin UI. After shopify app deploy:
Go to Settings → Shipping and delivery
Scroll to Customizations section at the bottom
Click Add customization → select your Function
Save
The hide rule is live in production. No mutation required.

Tutorial 3: Replacing a Payment Script (Hide COD for B2B)
Old Script: "Hide Cash on Delivery for any customer tagged 'B2B'." Here's the Payment Customization version.
Step 3.1: Scaffold
shopify app generate extension --template payment_customization --name hide-cod-b2b-fn
Step 3.2: Input query
query Input { cart { buyerIdentity { customer { hasTags(tags: [{ tag: "B2B" }]) { tag hasTag } } } } paymentMethods { id name } }
Step 3.3: Logic (src/run.js)
const NO_CHANGES = { operations: [] }; export function run(input) { const tagCheck = input.cart?.buyerIdentity?.customer?.hasTags?.[0]; const isB2B = tagCheck?.hasTag === true; if (!isB2B) return NO_CHANGES; const codMethod = input.paymentMethods.find((pm) => pm.name.toLowerCase().includes("cash on delivery") ); if (!codMethod) return NO_CHANGES; return { operations: [{ hide: { paymentMethodId: codMethod.id } }], }; }
Step 3.4: Activate
Payment Customizations also have an Admin UI under Settings → Payments → Customizations. Same flow as delivery — pick your Function, save, done.
Testing Strategy: The Tagged Customer Pattern
Functions don't have a "draft mode" you can toggle in admin. The professional pattern is to gate the new Function on a customer tag, run both the old Script and the new Function in parallel, verify they produce identical output for tagged users, then flip the switch.
Step 1: Tag your test users
In Customers, add the tag FN-TESTER to two or three internal accounts.
Step 2: Branch the Function on tag presence
// At the top of your run function let is_tester = input .cart() .buyer_identity() .and_then(|bi| bi.customer()) .map(|c| c.has_any_tag()) .unwrap_or(&false); if !*is_tester { return Ok(default_result); // Fall through to existing Script } // New Function logic only runs for tagged users
Step 3: Add hasAnyTag to your input query
cart { buyerIdentity { customer { hasAnyTag(tags: ["FN-TESTER"]) } } }
Step 4: Verify in checkout
Log in as a tagged user, walk through checkout, confirm the Function fires. Log in as an untagged user, confirm the old Script still runs. When parity holds for a few days, remove the tag check and let the Function run for everyone.
Step 5: Unpublish the old Script
Go to Apps → Script Editor → [Your Script] → Unpublish. Once unpublished, the Function is the sole source of truth.
Deployment Workflow That Actually Scales
Don't deploy from a developer's laptop forever. Once you've migrated one or two Scripts, set up a real CI pipeline.
The minimum viable workflow
# .github/workflows/deploy-functions.yml name: Deploy Shopify Functions on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '20' } - uses: dtolnay/rust-toolchain@stable with: { targets: wasm32-wasip1 } - run: npm install -g @shopify/cli@latest - run: shopify app deploy --force env: SHOPIFY_CLI_PARTNERS_TOKEN: ${{ secrets.SHOPIFY_CLI_PARTNERS_TOKEN }}
Generate the partner token from your Partner dashboard under Settings → Tokens. Now every merge to main ships your Functions. No more "did Mike deploy that?" Slack threads.
Versioning and rollback
shopify app deploy creates a versioned snapshot. To roll back:
shopify app versions list shopify app release --version <previous-version-id
Compared to Scripts — where rollback meant "remember the old code and paste it back in" — this is night and day.

What Most Teams Get Wrong
After helping Plus merchants through this migration over the past year, the same five mistakes show up.
1. Treating Functions like a 1:1 Script port. They're not. A single Function can replace three Scripts with cleaner branching. Audit your Scripts as a system before rewriting.
2. Forgetting the read scopes. Many Functions need read_customers, read_orders, or write_discounts. Add them in shopify.app.toml under scopes and re-authorize the app, or your input query returns null.
3. Running Functions on un-tagged customers without parity testing. Even if your code looks right, edge cases (empty cart, gift cards, store credit, B2B drafts) will surface problems. Tag-gated rollout costs you two days and saves you a P1 outage.
4. Skipping the Customizations Report. It's the single best inventory of what you actually have running. Don't migrate from memory — migrate from the report.
5. Hardcoding collection IDs and customer tags. Use Function configuration via metafields if you need merchant-tunable values. The CLI can scaffold metafield-backed config — see Shopify's docs on Function configuration.
Migration Checklist for the Next 75 Days
A realistic week-by-week plan to get to June 30 calmly.
Week | Action |
|---|---|
Week 1 (this week) | Pull the Customizations Report. Inventory every Script. Decide Function vs public app vs delete-it. |
Weeks 2–3 | Set up local dev environment. Scaffold the first Function. Migrate the simplest Script (usually a payment hide rule). |
Weeks 4–6 | Migrate discount Scripts. These take longest because the Discounts API is the richest. Tag-test thoroughly. |
Weeks 7–8 | Migrate shipping/delivery Scripts. Activate Delivery Customizations via Admin. |
Weeks 9–10 | Set up CI pipeline. Move all deployments off developer laptops. |
Weeks 11 (mid-June) | Final parity verification. Unpublish all Scripts. Run the store on Functions only for two weeks. |
June 30 | Sunset day arrives. Nothing breaks because you finished early. |
If you start this week, you have buffer. If you start in June, you don't.

Frequently Asked Questions
Do I need Shopify Plus to use Functions?
Custom Functions require Shopify Plus, but public-app Functions work on every plan. If you're not on Plus, you have two paths: install a public app from the Shopify App Store that ships the Function for you, or upgrade to Plus to write your own custom Functions. Most large merchants who had Scripts already had Plus, so this rarely changes anything practically.
Can I write Functions in TypeScript?
Yes — TypeScript is fully supported and the CLI scaffolds it for you. When you run shopify app generate extension and choose "JavaScript," the generated project includes type declarations from import("../generated/api"). You can convert files to .ts and add a tsconfig.json if you prefer. The compiled output (WASM) is identical regardless of source language.
How fast are Functions compared to Scripts?
Functions execute in under 5ms typically — significantly faster than Ruby Scripts. Because they're compiled to WebAssembly and run in a tight runtime, Shopify enforces a 5ms execution budget. If your Function exceeds it, the operation is dropped and your Function returns no operations. In practice, a well-written Function uses 1–2ms. The performance ceiling is much higher than Scripts.
Can a Function call an external API?
No — Functions cannot make network requests. They are pure computation: input cart data → output operations. If you need external data (a CRM lookup, a real-time inventory check), you'll need to either store the data in metafields ahead of time, or use a different surface (App Proxy, webhooks, Cart Transform with backend lookup). This is the most common reason teams need to redesign rather than port.
What's the difference between Cart Transform and Discounts?
Discounts apply price changes; Cart Transform changes the cart's contents. Use the Discounts API to apply a 10% off, free shipping, or BOGO. Use Cart Transform to bundle two products into one line item, or split one variant into multiple. Many old Scripts mixed both concerns — when migrating, separate them into two Functions.
How do I test a Function locally without a dev store?
You can run unit tests with cargo test (Rust) or npm test (JS), but full integration testing requires a dev store. The CLI provides shopify app function run which executes your Function against a sample input file — useful for fast iteration. But to confirm checkout behavior end-to-end, you need a real store with a real cart.
Can I have multiple Functions of the same type?
Yes — Shopify supports multiple Functions per target, and they execute in deterministic order. For discounts, the order is governed by Shopify's discount stacking rules. For Delivery and Payment Customizations, you can chain Functions but each output feeds the next. Most teams use one Function per type for simplicity.
What happens to my Script after I deploy the Function?
Both run in parallel until you unpublish the Script in Apps → Script Editor. This is intentional — it gives you the parallel-run testing window. After verifying the Function works, manually unpublish the Script. After June 30, 2026, all Scripts stop executing whether you unpublished them or not.
Will the migration affect my SEO or theme?
No — Functions execute server-side at checkout and never touch your theme or product pages. They only modify discounts, shipping options, and payment methods at checkout. Your storefront, product templates, and SEO are completely untouched.
How do I migrate a Script that uses Input.line_items with custom properties?
Custom properties are accessible via the attribute field on cart lines in the GraphQL input. Add attribute(key: "your-key") { value } inside the lines selection. The Function reads it identically to how Scripts read line item properties — just through GraphQL instead of Ruby method calls.
What about analytics and order tags? Can Functions write data?
Functions cannot write order tags or trigger webhooks themselves — they only return operations on the current cart. For tagging or downstream workflows, use Shopify Flow triggered by the order creation event. Many merchants combine a Function (for the discount) with a Flow (for tagging "VOLUME-DISCOUNT-APPLIED" on the order).
Is there a public app I can install instead of building a custom Function?
Yes — the Shopify App Store has dozens of apps that wrap Functions for common use cases. Search for "discount function," "delivery customization," or "payment customization." For straightforward use cases (volume discounts, hide payment methods by tag, free shipping over X), an existing app may save you days of dev work. Reserve custom Functions for logic that's truly unique to your business.
What if I miss the June 30 deadline?
The Script stops executing — there is no fallback, no grace period, and no extension. Whatever the Script was doing (the discount, the hidden shipping rate, the blocked payment method) reverts to default behavior at midnight UTC on July 1. If your business depends on that logic, plan to be live well before that date. The migration takes longer than most teams estimate, especially with parity testing.
Can I delete the Script Editor app entirely?
You can uninstall it after June 30, 2026, but Shopify will likely auto-remove it. Once Scripts stop executing, the editor serves no purpose. You can also unpublish all Scripts now and uninstall the app immediately if you've completed migration — your Functions are independent.
What to Do This Week
Don't read this article and close the tab. Do these four things in the next seven days.
1. Pull the Customizations Report. Go to Settings → Checkout → Customizations Report. Export it. This is your migration backlog.
2. Set up your dev environment. Install Node 18+, Shopify CLI, and Rust (if applicable). Confirm shopify version works. Total time: 30 minutes.
3. Scaffold and ship one tiny Function to a dev store. Pick the simplest Script you have — usually a payment hide rule. Migrate it end to end. Even if it never goes to production, you've validated the toolchain.
4. Block calendar time for the next eight weeks. Migrations don't happen in spare moments between sprints. Carve out a recurring slot — every Tuesday and Thursday afternoon, for example — and treat it like a release.
From the teams we've seen migrate, the pattern is consistent: two weeks of stalling, three weeks of actual work, one week of cleanup. That's six weeks. You have ten. The slack is there — burn it on code review and QA, not on putting off the start.
And once your checkout logic is sorted, the next thing most teams tackle is post-purchase — customer address changes, swaps, discount additions after the order is placed. If that's on your roadmap (and it should be, whether you're a high-volume Plus operator or a CX-led Advanced store), Revize is on the Shopify App Store and works alongside every Function you'll build here.
Resources
Related Articles
Revize your Shopify store, and lead with
customer experience
© Copyright 2024, All Rights Reserved
Revize your Shopify store, and lead with
customer experience
© Copyright 2024, All Rights Reserved
Revize your Shopify store, and lead with
customer experience
© Copyright 2024, All Rights Reserved
Revize your Shopify store, and lead with
customer experience
© Copyright 2024, All Rights Reserved



