Shopify Functions vs Shopify Scripts: A Migration Walkthrough
By Codcompass Team··8 min read
Declarative Checkout Logic: Architecting the Shopify Functions Migration
Current Situation Analysis
The Shopify checkout customization landscape is undergoing a forced architectural transition. Shopify Scripts, the legacy Ruby-based hook system that has powered Plus merchant checkout logic since 2016, reaches its edit lock on April 15, 2026, and will cease execution entirely on June 30, 2026. Merchants operating on the Plus tier are now navigating a compressed runway to migrate existing logic to Shopify Functions.
The core friction point is not the deprecation timeline itself, but the fundamental mismatch in execution contracts. Legacy Scripts operate on an imperative model: Ruby code executes within the checkout process, directly mutates a Cart object in memory, and returns the modified state. Functions, by contrast, operate on a declarative, WebAssembly-backed model. They receive a strictly scoped GraphQL payload, execute in an isolated sandbox, and return a list of operations that Shopify's checkout engine applies. You do not mutate state; you declare intent.
This paradigm shift is frequently overlooked during migration planning. Engineering teams often attempt line-by-line porting, treating Functions as a drop-in replacement for Scripts. This approach fails because Functions enforce strict resource boundaries, require explicit data fetching via GraphQL, and mandate explicit discount stacking strategies. The administrative convenience of the Script Editor also masks technical debt: legacy code is rarely version-controlled, poorly documented, and frequently contains overlapping or contradictory rules. Without a structured audit and architectural realignment, migrations routinely exceed timelines and introduce checkout instability.
The transition is not merely a language swap from Ruby to JavaScript/TypeScript/Rust. It is a migration from monolithic, state-mutating hooks to edge-optimized, declarative operation generators. Understanding this distinction is the prerequisite for a successful cutover.
WOW Moment: Key Findings
The migration requires abandoning direct state manipulation in favor of operation declaration. The table below contrasts the architectural constraints and capabilities of both systems, highlighting why legacy patterns fail in the Functions runtime.
This comparison reveals why direct porting fails. Functions are designed for predictable, sandboxed execution at scale. The GraphQL input contract prevents accidental data leakage, the instruction budget guarantees checkout latency stability, and the declarative return structure ensures idempotent application of business rules. Merchants who align their migration strategy with these constraints avoid checkout regressions and leverage Shopify's modern extension architecture.
Core Solution
A successful migration follows a disciplined workflow that prioritizes architectural alignment over code translation. The process breaks down into five phases: audit and native mapping, extension
point selection, GraphQL contract definition, declarative implementation, and fixture-driven validation.
Phase 1: Audit and Native Feature Mapping
Before writing a single Function, catalog every active Script. Document the business rule, target audience, and edge cases. Cross-reference each rule against Shopify's native discount and checkout customization features. Many legacy Scripts replicate functionality now available natively (automatic discounts, discount combinations, shipping rate filters). Eliminating these reduces migration scope and technical debt.
Phase 2: Extension Point Routing
Remaining logic must be routed to the correct Function extension point:
Delivery Customization Functions: Shipping method filtering, renaming, reordering, or hiding.
Payment Customization Functions: Payment method filtering or reordering.
Cart Transform Functions: Line item expansion, bundle decomposition, or component mapping.
Routing logic early prevents architectural mismatches. A shipping surcharge rule belongs in a Delivery Function, not a Discount Function, even if it affects the final total.
Phase 3: GraphQL Contract Definition
Functions do not receive the entire cart. They receive exactly what you request in input.graphql. This query acts as a strict contract between the checkout engine and your WebAssembly module. Missing fields are not null; they are absent from the payload. Define the query conservatively but completely. Over-fetching increases payload size and risks hitting the 125KB input cap. Under-fetching causes runtime undefined errors.
Phase 4: Declarative Implementation
Implement the business logic as a pure function that transforms the GraphQL input into an operations payload. Avoid loops that scale with cart size. Use early returns for zero-impact scenarios. Explicitly declare targets and stacking strategies.
Phase 5: Fixture Validation and Deployment
Never test Functions against production checkout. Use shopify app function run with JSON fixture files that mirror real cart shapes, including edge cases (empty carts, maximum line items, mixed currencies). Validate operation output against expected checkout totals before promoting to production.
Code Example: Category-Filtered Volume Discount
The following TypeScript implementation demonstrates a declarative discount that applies a 15% reduction when a cart contains 5+ items from a specific product category. Note the explicit GraphQL query, the operation return structure, and the absence of state mutation.
Early Exit: The function returns immediately if the target tag is absent or quantity threshold isn't met, preserving instruction budget.
Explicit Targeting: Instead of iterating and mutating line prices, we construct a targets array that maps variant IDs to quantities. Shopify's checkout engine applies the discount atomically.
Strategy Declaration: discountApplicationStrategy: "FIRST" ensures this discount applies before subsequent rules, preventing unexpected stacking behavior.
Type Safety: Generated TypeScript types from input.graphql catch missing fields at compile time, eliminating runtime undefined errors.
Pitfall Guide
1. The GraphQL Blind Spot
Explanation: Developers assume all cart data is available by default. Functions only expose fields explicitly requested in input.graphql. Missing fields are omitted from the payload, not set to null.
Fix: Treat input.graphql as a strict schema. Use TypeScript code generation (graphql-codegen) to enforce type safety. Validate payload structure in unit tests before deployment.
2. Instruction Budget Blowouts
Explanation: Functions enforce a ~11 million instruction limit. Complex regex, nested .find() chains, or loops over entire catalogs exceed this budget, causing silent failures or checkout timeouts.
Fix: Profile execution early. Replace regex with string matching or precomputed metafields. Flatten nested loops. Use early returns to short-circuit processing.
3. Implicit Stacking Assumptions
Explanation: Scripts applied discounts in execution order, often stacking implicitly. Functions require explicit discountApplicationStrategy (FIRST or MAXIMUM). Choosing incorrectly alters final totals.
Fix: Map legacy stacking behavior to the correct strategy. FIRST applies discounts sequentially. MAXIMUM applies only the highest-value discount. Document the chosen strategy in the function manifest.
4. The HTTP Mirage
Explanation: Functions run in a WebAssembly sandbox with no network access. Developers occasionally assume outbound HTTP calls work because Functions are deployed as part of an app.
Fix: Pre-fetch external data and store it in product metafields or customer tags. Query metafields via input.graphql. If real-time data is required, shift logic to a checkout extension or post-purchase flow.
5. Fixture Coverage Gaps
Explanation: Testing only against happy-path carts misses edge cases like empty carts, maximum line item limits, or mixed currency scenarios. Production failures occur when untested payloads hit the function.
Fix: Generate fixture files programmatically using a cart builder script. Include boundary conditions: 0 items, 100+ items, zero-price variants, and disabled payment methods. Run fixtures in CI on every commit.
6. Extension Point Mismatch
Explanation: Applying discount logic inside a Delivery Function, or shipping filters inside a Discount Function, causes validation errors or silent rule suppression.
Fix: Strictly separate concerns. Discount Functions modify pricing. Delivery Functions modify shipping options. Payment Functions modify payment methods. Cart Transform Functions modify line structure. Route rules during the audit phase.
7. Currency Hardcoding
Explanation: Dev stores often default to a single currency. Hardcoding CAD or USD in discount calculations breaks multi-currency stores during Black Friday or regional campaigns.
Fix: Query cart.cost.totalAmount.currencyCode from the GraphQL input. Normalize calculations to the cart's currency. Test fixtures with multiple currency codes.
Production Bundle
Action Checklist
Audit legacy Scripts: Document every active rule, edge case, and business owner.
Map to native features: Replace replicable logic with automatic discounts or combination rules.
Route to extension points: Assign remaining rules to Discount, Delivery, Payment, or Cart Transform functions.
Draft GraphQL contracts: Define input.graphql queries with strict field selection.
Implement declarative logic: Write pure functions that return operations, not mutations.
Scaffold the project: Run shopify app function create and select the appropriate extension point (e.g., discount). The CLI generates the project structure, shopify.function.toml, and TypeScript types.
Define the GraphQL contract: Edit src/input.graphql to request only the fields your logic requires. Run npm run generate to produce TypeScript types from the query.
Implement the run function: Replace the placeholder logic in src/run.ts with your declarative operation builder. Use early returns, explicit targeting, and the correct discountApplicationStrategy.
Test with fixtures: Create fixtures/input.json containing a representative cart payload. Execute shopify app function run --input fixtures/input.json and verify the operation output matches expected checkout behavior.
Deploy to dev store: Run shopify app deploy to push the Function to your development store. Validate in the checkout UI using Shopify's preview mode before promoting to production.
🎉 Mid-Year Sale — Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all 635+ tutorials.