What actually stopped
Two dates matter. On April 15, 2026 the Script Editor went read-only: no edits, no new Scripts. On June 30, 2026 every Script stopped executing. Shopify was explicit that this was the final date after two earlier extensions, and there's no grace period and no way to switch a Script back on.
Scripts came in three types, and each one mapped to a specific moment in checkout:
- Line item Scripts changed prices on cart lines. Volume tiers, bundle pricing, "buy 3 get 1", customer-tag pricing, discounts that only applied to some SKUs.
- Shipping Scripts hid, renamed, reordered or repriced shipping rates. Free shipping over a threshold for one region only, hiding express for oversized items, renaming a carrier rate to something customers recognise.
- Payment Scripts hid, renamed or reordered payment methods. Hiding cash on delivery outside a country, pushing a preferred gateway to the top, hiding a method for low-value carts.
Whatever was still running as a Script on June 30 simply stopped, and the store carried on as if nothing happened. Checkout loaded, orders came in, nobody got an error. I'd argue that's worse than an outage, because an outage gets noticed the same hour. This one shows up weeks later, in the orders, if someone goes looking.
The three places it goes wrong quietly
Most Plus stores did migrate something before the deadline. The trouble I keep seeing is the migration that's 80% done. A Function replaced the Script, the obvious cases were tested, and nobody checked the edges. Three patterns account for most of it.
1. Two discounts that used to stack now don't
A line item Script could apply a volume discount and then apply a promo code on top, in one pass, because the Script owned the whole line. Functions don't work like that. Shopify applies at most one product-class discount per line. If your volume pricing and your promo code are both product discounts, the larger one wins and the other one disappears from the line. No error, no warning. The customer just gets less than the Script gave them, or the store gives more than it meant to, depending on which discount is bigger.
The fix is to move one of the two to the order class, which is its own topic. Guide 2 covers it in full.
2. Shipping rules that lived in the Script, not in the rate table
Shipping Scripts often carried rules that nobody wrote down anywhere else: hide the express rate when the cart has a fragile item, rename "UPS Ground" to "Standard (3 to 5 days)", show free shipping only when every line qualifies. When the rate engine was migrated, the rates came across because they're in the admin. The rules didn't, because they were in the Script. After June 30 the customer sees every rate the carrier returns, under the carrier's names.
3. Payment method order and hiding
The least visible of the three. A payment Script that hid a method for carts under a threshold, or moved a gateway to the top, has no equivalent in the admin at all. When it stopped, the checkout went back to Shopify's default order. Conversion effects are small per order and large in aggregate, and nobody notices for weeks because the checkout still looks fine.
How to find the gap in your own orders
Nobody has to remember what the Scripts did. The orders already know. While a Script ran, every order it touched got a discount application with the type ScriptDiscountApplication. After June 30 that type can't appear. So compare a window of orders before the cutoff with a window after, and see what moved.
Here's the Admin GraphQL query I use. Run it twice, once per window.
query OrdersWindow($q: String!, $cursor: String) {
orders(first: 100, after: $cursor, query: $q, sortKey: CREATED_AT) {
pageInfo { hasNextPage endCursor }
nodes {
name
createdAt
currentSubtotalPriceSet { shopMoney { amount } }
currentTotalDiscountsSet { shopMoney { amount } }
discountApplications(first: 10) {
nodes {
__typename
targetType
value {
... on PricingPercentageValue { percentage }
... on MoneyV2 { amount }
}
}
}
shippingLine { title originalPriceSet { shopMoney { amount } } }
paymentGatewayNames
}
}
}
For the before window the query string is created_at:>=2026-05-15 created_at:<2026-06-30, for after it's created_at:>=2026-07-01 created_at:<2026-08-15. Page through, collect the nodes, then this bit of Node does the comparison:
function summarise(orders) {
const out = { orders: orders.length, scriptDiscounts: 0, otherDiscounts: 0,
discountRate: 0, shippingTitles: {}, gateways: {} };
let subtotal = 0, discounts = 0;
for (const o of orders) {
subtotal += Number(o.currentSubtotalPriceSet.shopMoney.amount);
discounts += Number(o.currentTotalDiscountsSet.shopMoney.amount);
for (const d of o.discountApplications.nodes) {
if (d.__typename === 'ScriptDiscountApplication') out.scriptDiscounts++;
else out.otherDiscounts++;
}
const t = o.shippingLine?.title ?? '(none)';
out.shippingTitles[t] = (out.shippingTitles[t] ?? 0) + 1;
for (const g of o.paymentGatewayNames) out.gateways[g] = (out.gateways[g] ?? 0) + 1;
}
out.discountRate = subtotal ? +(discounts / subtotal).toFixed(4) : 0;
return out;
}
console.table([summarise(before), summarise(after)]);
Read three things off the table. discountRate down after June 30, with otherDiscounts not climbing to cover the lost scriptDiscounts? A line item Script died and nothing took its place. New entries in shippingTitles (raw carrier names customers were never meant to see), or the old renamed ones gone? Shipping rules died. A different mix in gateways? The payment Script died.
One trap: seasonality. If the before window has a sale in it and the after window doesn't, the discount rate drops for reasons that have nothing to do with Scripts. Pick two plain six-week windows, or split the numbers by campaign first.
Rebuilding each Script as a Function
Each of the three Script types maps to exactly one Function target, and all three work the same way. You write a small GraphQL query that says which cart fields you want, and a run function that gets those fields and returns a list of operations. Shopify runs it inside checkout on its own infrastructure, so there's nothing to host and no timeout to worry about (a Script ran on Shopify's side too, so this part will feel familiar).
| Script type | Function target | What it can do |
|---|---|---|
| Line item | cart.lines.discounts.generate.run | Add product discounts (per line) or order discounts (on the subtotal), accept or reject entered codes |
| Shipping | cart.delivery-options.transform.run | Hide, rename and reorder delivery options |
| Payment | cart.payment-methods.transform.run | Hide, rename and reorder payment methods, set payment terms |
Say the Script gave 10% off orders over 200. Here's the same rule as a Function. The query asks for the subtotal and nothing else, and the function returns a single order discount candidate once the subtotal is over the line.
// run.graphql
query Input {
cart {
cost { subtotalAmount { amount } }
}
}
// run.js
export function cartLinesDiscountsGenerateRun(input) {
const subtotal = Number(input.cart.cost.subtotalAmount.amount);
if (subtotal < 200) return { operations: [] };
return {
operations: [{
orderDiscountsAdd: {
selectionStrategy: 'FIRST',
candidates: [{
message: '10% off orders over 200',
targets: [{ orderSubtotal: { excludedCartLineIds: [] } }],
value: { percentage: { value: '10.0' } },
}],
},
}],
};
}
A shipping Script that hid express rates for a heavy cart becomes a delivery customization. Same idea, different operations:
export function cartDeliveryOptionsTransformRun(input) {
const heavy = input.cart.lines.some((l) => (l.merchandise.weight ?? 0) * l.quantity > 20000);
if (!heavy) return { operations: [] };
const operations = [];
for (const group of input.cart.deliveryGroups) {
for (const option of group.deliveryOptions) {
if (/express|overnight/i.test(option.title ?? '')) {
operations.push({ deliveryOptionHide: { deliveryOptionHandle: option.handle } });
}
}
}
return { operations };
}
Payment customizations follow the same pattern with paymentMethodHide, paymentMethodMove and paymentMethodRename. The one thing that catches people: a Function has to be deployed inside an app (a custom app on your store is fine), then activated from the admin. Deploying it doesn't turn it on.
Migrate one class at a time. Discounts first, because that's where the money is. Then shipping, then payment. Keep the old Script's rules written down next to the Function's tests, so the next person can see what the Function is supposed to reproduce.
The stacking trap
Worth repeating because it's the one that costs the most. Scripts let you stack whatever you wanted on a line. Functions respect Shopify's discount classes: product, order and shipping. Two discounts of the same class never combine on one line, and the combinesWith settings only control combining across classes. If you migrated a Script that stacked two product-level discounts, your Function is doing less than the Script did, and the only fix is to make one of them an order discount that computes its amount from the qualifying lines only. That's a small Function, but it's a different one from what most people migrated to.
A 30-minute audit you can run today
If you do one thing after reading this, do the list below. Half an hour, admin access, a test card.
- Open the Scripts customization report in the admin (it's still visible) and list every Script that existed on April 15. That's your target list.
- For each Script, find the Function or app that's supposed to replace it. If you can't name one, that's a gap.
- Run the orders query above for a before and after window. Compare discount rate, shipping titles and gateway mix.
- Place a test order that should trigger every discount rule. Check the line totals against what the Script would have produced. Do it with two discounts at once.
- Add a heavy or oversized item and check which shipping rates show. Check the names.
- Change the shipping country and check the payment methods and their order.
- Check that each Function is activated, not just deployed. The admin shows both states and they look similar.
- Write down which rules are now in which Function, in the repo, next to the tests.
Anything that fails a step is a missing Function, or a Function that does less than the Script did. Either one is a few days of work. Not knowing is what gets expensive.