Shopify Functions vs Scripts: What Actually Changes When You Migrate Discount Logic
Shopify Scripts are on borrowed time. Here's what breaks, what improves, and what you'll wish you knew before rewriting your discount logic as Shopify Functions.

If you run a Shopify Plus store with any real discount logic, you've probably known this day was coming. Shopify Scripts are being retired in favor of Shopify Functions, and the migration isn't a find-and-replace job. It's a rewrite that changes where your logic lives, how it's tested, and what your merchandising team can and can't do at 2am on Black Friday.
We've moved several Plus merchants across this line in the last year. Here's the honest breakdown.
Why Shopify is Forcing the Move
Scripts ran on a Ruby sandbox inside Shopify's checkout. That checkout is being replaced by the new extensible checkout (Checkout Extensibility), which doesn't run Scripts at all. Functions are the replacement: WebAssembly modules, written in Rust, JavaScript, or TypeScript, that execute inside Shopify's infrastructure with strict CPU and memory limits.
The pitch from Shopify is faster execution, better observability, and a proper deployment pipeline via Shopify CLI. The reality is a bit more nuanced.
What actually goes away
Script Editorapp, the in-admin Ruby editorline_items.eachstyle mutation of the cart from a script- The ability for a non-developer to paste code into production
- Free-form access to the full customer object at checkout time
That last one is the sharpest edge. Scripts had broad access. Functions receive a scoped GraphQL input you define up front. If you didn't ask for customer.metafields.loyalty.tier in your input query, you don't get it at runtime.
The Mental Model Shift
Scripts were imperative. You looped over line items, mutated prices, and returned. Functions are declarative: you return a list of operations that Shopify's checkout engine applies. You don't set a price; you return a productDiscount with a target and a value.
Here's what a trivial "10% off any item tagged clearance" Function looks like in TypeScript:
import type { RunInput, FunctionRunResult } from "../generated/api";
export function run(input: RunInput): FunctionRunResult {
const discounts = input.cart.lines
.filter((line) => {
const product = line.merchandise.__typename === "ProductVariant"
? line.merchandise.product
: null;
return product?.hasAnyTag ?? false;
})
.map((line) => ({
targets: [{ cartLine: { id: line.id } }],
value: { percentage: { value: 10.0 } },
}));
if (discounts.length === 0) {
return { discounts: [] };
}
return {
discounts: [
{
message: "Clearance 10% off",
targets: discounts.flatMap((d) => d.targets),
value: { percentage: { value: 10.0 } },
},
],
discountApplicationStrategy: "FIRST",
};
}
And the input query that defines what your Function receives:
query RunInput {
cart {
lines {
id
quantity
merchandise {
__typename
... on ProductVariant {
id
product {
hasAnyTag(tags: ["clearance"])
}
}
}
}
}
}
Notice: no network calls, no database lookups. Whatever your Function needs, it must be in that input query or in metafields you've explicitly requested.
Where Migrations Actually Get Painful
The simple cases (percentage off a collection, BOGO on a tag) are a weekend. The pain lives in three areas.
1. Logic that depended on external state
A lot of Scripts we've seen made assumptions like "if the customer has ordered more than five times, give free shipping." In Scripts, customer.orders_count was just there. In Functions, you have two options:
- Store the value in a customer metafield, updated by a webhook on
orders/create - Skip the Function and use a Shopify Flow + automatic discount combo
Metafields are the more common answer. Which means your discount migration becomes a data pipeline project: you need a worker that listens to order webhooks, updates metafields, handles retries, and backfills history. That's usually where the timeline doubles.
2. Cart-wide conditional bundles
Scripts could look at the whole cart and rewrite prices freely. Functions can too, but the 5ms-ish CPU budget and 256KB input size are real. We've hit the input limit on stores with 40+ line items and heavy variant metafields. The fix is trimming your input query aggressively; if you're querying a metafield you use in one branch, gate it behind a @include directive or split into multiple Functions with different targets.
3. Merchandiser workflow
This is the underrated one. With Scripts, a merchandiser could tweak a discount threshold in the admin. With Functions, that's a git commit, CI run, and shopify app deploy. Merchants hate this.
The workaround we've settled on: expose configuration via metafields on the discount itself, and build a small admin UI (a Shopify admin extension or a simple embedded app) that lets non-developers change the values. Your Function reads discountNode.metafield at runtime.
query RunInput {
discountNode {
metafield(namespace: "config", key: "tiers") {
value
}
}
cart { ... }
}
That one pattern preserves 80% of the flexibility merchandisers had with Scripts.
Performance and Observability
In our experience, Functions execute in a few milliseconds for typical carts. Shopify enforces a hard instruction limit (currently around 11 million WASM instructions), and if you blow past it, the Function fails silently and no discount applies. That's a genuinely dangerous failure mode on a promo day.
Two things we do on every project:
- Log via
logsin the FunctionRunResult and stream them through Shopify CLI during load testing - Add a synthetic monitor that adds a known cart combination every 5 minutes and asserts the discount lines are present via the Storefront API
Don't rely on the Partner Dashboard's Functions log alone. It's fine for debugging but not for alerting.
Should You Move Everything to Functions?
No. Functions are the right home for:
- Discount logic that runs at checkout
- Delivery customizations (rename, reorder, hide shipping methods)
- Payment customizations (hide payment methods based on cart contents)
- Cart transforms (bundle expansion)
But a lot of what people crammed into Scripts belongs elsewhere now:
- Customer segmentation → Shopify Segments + automatic discounts
- Time-limited sitewide sales → automatic discount with schedule, no code needed
- Loyalty tier logic → dedicated app or metafield + Function combo
- Free gift with purchase → the new native GWP or a cart transform Function
We've seen teams try to port a 400-line Script one-for-one and end up with a Function that's both slower to iterate on and less capable than just using two native features together.
A Rough Migration Playbook
What's worked for us on Plus migrations:
- Inventory every Script. Not just what it does, but which merchandiser owns it and how often it changes.
- Classify each one: native feature, Function, Flow automation, or delete (you'll find a few that no longer apply).
- Build the metafield pipeline first if any Script depends on customer or order history. This is your critical path.
- Rewrite Functions in priority order, starting with the ones that change least often.
- Run both in parallel on a duplicate checkout during QA. Compare discount lines on a corpus of real historical carts.
- Cut over one Function at a time, not all at once. Shopify lets you enable/disable per discount.
Budget more time than you think for step 3 and for the merchandiser admin UI. Those are the parts that get skipped in estimates and remembered in retros.
Where We'd Start
If you're staring at a folder of Scripts and a deprecation notice, don't open your IDE first. Open a spreadsheet. List every Script, its owner, its last-changed date, and whether it depends on data outside the cart. That single document will tell you whether this is a two-week project or a two-quarter one.
Then build one Function end-to-end — the smallest, least critical discount you have — and take it all the way through CI, deployment, and monitoring. You'll surface every tooling gap in your org before you're doing it under pressure on the discount that actually matters.
If you want a second pair of eyes on the plan, our e-commerce team has done this dance a few times.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

Shop Pay vs Custom Checkout: The Real Cost of Owning Your Funnel
Shop Pay converts. Custom checkouts flex. Here's how to decide which one to run in 2026 — and what the migration actually costs when you get it wrong.

Cart Abandonment Recovery: Why Your Email Flow Is Losing to SMS and What to Rebuild
Email-only abandoned cart flows are quietly bleeding revenue in 2026. Here's how we rebuild them around SMS, event timing, and identity — and what actually recovers carts.

Collection Page Filtering at Scale: Why Your Shopify Facets Are Killing Conversion
Faceted filtering on large Shopify collections quietly destroys mobile conversion. Here's how the requests stack up, why theme defaults fall over past 500 SKUs, and what actually holds up in production.
