FUNCTIONS · CHECKOUT · TECHNICAL · 2 APRIL 2026 · 10 MIN READ
Custom checkout logic with Functions: three worked examples
Functions run inside Shopify’s own checkout, which is why they can do things no script in your theme can — and why they cannot look at a clock.
Shopify Functions let you insert your own logic into the parts of checkout Shopify owns: discounts that no discount form can express, delivery and payment options that appear or disappear based on the cart, validation that blocks a checkout server-side, bundles assembled at the cart, fulfilment and routing rules. They run as WebAssembly inside Shopify’s infrastructure, which buys you logic that cannot be bypassed and costs you the things a normal backend takes for granted — no clock, no randomness, a 20 kB output budget and an 11 million instruction ceiling. Before scoping any of it, check the plan gate: Shopify documents that only stores on a Shopify Plus plan can use custom apps containing Function APIs.
IN SHORT
- Shopify documents that "only stores on a Shopify Plus plan can use custom apps that contain Shopify Function APIs" — public apps from the App Store are available on all plans.
- Functions can be written in any language that compiles to WebAssembly; Shopify provides Rust and JavaScript templates and "strongly recommends Rust as the most performant language choice to avoid your function failing with large carts".
- Nondeterminism is not allowed: "you can't use any randomizing or clock functionality in your functions", so anything time-dependent must be passed in as data.
- The documented resource limits for carts up to 200 line items are 11 million execution instructions, 128 kB of input and 20 kB of output, with a compiled binary limit of 256 kB.
- The output limit "doesn't support bulk price transformations across all line items", which shapes how a discount function must target lines.
- The input query is capped at 3000 bytes with a maximum calculated cost of 30, and metafields over 10,000 bytes are not returned.
What a Function actually is
A Function is a small piece of code you write, compiled to WebAssembly, that Shopify executes at a defined point in its own checkout flow. It receives a structured input — the parts of the cart, customer and shop you asked for in an input query — and returns a structured output describing what should change: these discounts apply, hide that shipping method, block this checkout, group those lines into a bundle.
That "inside Shopify" part is the whole value proposition, and it is worth being concrete about why. Logic in your theme runs in the customer’s browser, where it can be inspected, edited or skipped, and it disappears entirely at the checkout step. Logic in an app that listens for webhooks runs after the order exists, which is too late to prevent anything. A Function runs before the order exists, on Shopify’s side of the boundary, and its answer is the one that counts.
Shopify documents the available APIs and the order they execute in: cart transform, discounts, fulfilment constraints, order routing, pickup point and local pickup delivery option generators, delivery customisation, payment customisation, and cart and checkout validation. Between them they cover most of the checkout behaviour that used to require Shopify Scripts, plus several things Scripts never could.
Example one: a discount rule the discount form cannot express
The brief that arrives most often: trade customers get tiered pricing by volume, the tiers differ by product group, and none of it may stack with a seasonal promotion already running on the site. Every part of that is ordinary retail logic and none of it fits in the discount form.
The shape of the Function is straightforward. Your input query asks for the cart lines, the merchandise and its product, the customer and whichever metafields hold the configuration — tier thresholds on the product or collection, the customer’s trade group on the customer record. The code walks the lines, works out which tier each qualifying line reaches, and returns discount targets with amounts.
Three details decide whether it survives a real promotion.
Put the configuration in metafields, not in the code. A tier table compiled into the binary means every threshold change is a deploy. The same table in metafields is editable by the person who owns pricing, and the Function reads it through the input query. This is the single biggest difference between a Function that works for a year and one that becomes a ticket every fortnight.
Respect the input query budget. The input query is limited to 3000 bytes excluding comments, with a maximum calculated cost of 30 — exceed it and validation fails with "Query has complexity of <cost>, which exceeds max complexity of 30". Metafields larger than 10,000 bytes are not returned at all, which rules out stuffing an entire price book into one JSON metafield and reading it per request. If your configuration is genuinely large, it belongs on the objects it describes rather than in one place.
Watch the output budget. Shopify caps function output at 20 kB for carts up to 200 line items and states plainly that this "doesn't support bulk price transformations across all line items". A trade order with a hundred lines and a discount emitted per line will run into that. Target the lines that need it, group where the API allows grouping, and test with the largest cart your wholesale customers actually place rather than with a cart of three.
The thing a Function will not solve is which discounts combine with which — that is configured on the discount, and it is where most promotions actually break. That is a separate problem with its own answer.
Example two: delivery options that reflect what is in the cart
Second brief: next-day delivery must disappear when the cart contains a made-to-order item, an aerosol, or anything shipping from the slower warehouse; and the remaining option should be renamed so the customer understands why.
This is a delivery customisation function — Shopify’s documented examples include hiding and renaming delivery options. The input query asks for the cart lines and a metafield on each product marking it as restricted, plus the delivery groups and their available options. The output hides or renames.
The interesting part is what you cannot do, and it catches teams out every time. There is no clock. Shopify does not allow nondeterminism in functions, explicitly including clock functionality, so a Function cannot ask what time it is and therefore cannot decide on its own that the cut-off for next-day has passed.
That is not a dead end, it is a design instruction: time-dependent decisions are made outside the Function and passed in as data. The cut-off logic lives in the theme or in an app that writes a cart attribute — *dispatch window closed*, or the computed delivery date — and the Function reads the attribute and acts on it. Shopify’s own guidance points the same way, noting you can pre-populate data using metafields on products and customers, or pass data using cart attributes.
Worth saying clearly, because this pattern is the answer to a whole class of requirements. Anything a Function needs to know that is not a property of the cart, the customer or the shop has to be put where the Function can see it — computed upstream, stored in a metafield or an attribute, read as input. Once a team internalises that, half of the "can Functions do X" questions answer themselves.
One honest caveat: pushing a rule into a cart attribute means the value arrives from the storefront, which is exactly the layer a Function exists to be independent of. Use attributes for things where being wrong is a poor experience rather than a loss — a delivery date, a cut-off — and keep anything with money or eligibility attached in metafields the customer cannot influence.
Example three: validation that actually blocks the order
Third brief: trade accounts have a minimum order value; certain products cannot ship to certain regions; and a configurable item is invalid unless its accompanying options are present. Today all three are enforced with JavaScript on the cart page, and every week somebody gets through anyway.
A cart and checkout validation function is the right tool — Shopify’s documented example is blocking progress on a checkout when cart line quantities exceed a limit. It returns errors, and an error stops the checkout. Not a warning, not a banner someone dismisses: the customer cannot proceed.
What makes this worth the effort is not the rule, which is trivial in any language. It is that the rule now has one home. The cart page check, the checkout check and the "we caught it in ops" check collapse into a single function running on Shopify’s side, and the failure mode changes from "an invalid order exists and someone must ring the customer" to "the order was never placed".
Two things to get right. Write errors a customer can act on. "Validation failed" is a support ticket; "Trade orders have a minimum of X units — add Y more to continue" is a resolved problem. The message is the entire interface of this Function, and it deserves the same attention as any other checkout copy. Decide what happens to a cart that becomes invalid after the fact — stock moves, a customer is reassigned to a different account, a product is restricted. The Function will correctly block a checkout for a cart the customer assembled in good faith last week, so the storefront should ideally be showing the same rule long before they reach checkout. The Function is the enforcement layer; it is not a substitute for telling people earlier.
The constraints, collected
These are documented, and reading them before you design is considerably cheaper than discovering them during testing. For carts up to 200 line items:
- 11 million execution instructions. Generous for ordinary logic, not generous for a nested loop over a large cart. Shopify recommends Rust specifically to avoid functions failing with large carts.
- 128 kB of input, 20 kB of output. The output cap is the one that bites, and Shopify notes it does not support bulk price transformations across all line items.
- 256 kB compiled binary, 10,000 kB of runtime linear memory, 512 kB of stack, and logs truncated at 1 kB — which makes debugging by logging a limited sport.
- Input query: 3000 bytes, maximum cost 30, list arguments capped at 100 elements, and metafields above 10,000 bytes are not returned.
- No randomness and no clock. Anything time-based or random is computed elsewhere and passed in.
- Any WebAssembly-compiling language, with Rust and JavaScript templates provided and Rust recommended for performance.
The plan gate, and the honest advice around it
Before any of this is scoped, one sentence in Shopify’s documentation decides whether the conversation is a development project or an app search: "Only stores on a Shopify Plus plan can use custom apps that contain Shopify Function APIs." Public apps distributed through the App Store are available on all plans.
So for a store not on Plus, a bespoke Function is not a line item on a quote — it is a plan upgrade plus a development project. That changes the maths completely, and in most of those conversations the right answer is an app from the store that already does something close enough, or a simplification of the rule so the native discount and shipping settings can express it.
That second option gets dismissed too quickly. A surprising number of Function briefs describe a rule that exists because of an operational constraint nobody has revisited, or a pricing structure that grew one exception at a time. "Three tiers instead of seven" is not a compromise if nobody can explain what tiers four through seven are for, and it is considerably cheaper to maintain than any amount of WebAssembly.
Where a Function does earn its cost, it earns it decisively: logic that cannot be bypassed, that survives theme changes, that runs in checkout rather than around it, and that moves a rule out of four places into one. Those are durable properties, and they are the reason this is one of the few pieces of Shopify’s extensibility surface we would recommend building on without much hesitation — once the plan question is settled.
Questions this raises
What can you build with Shopify Functions?
Shopify documents Function APIs for cart transform, discounts, fulfilment constraints, order routing, pickup point and local pickup delivery option generators, delivery customisation, payment customisation, and cart and checkout validation. In practice that covers custom discount types, hiding or renaming delivery and payment options, blocking a checkout server-side, assembling bundles, and influencing which location an order is assigned to.
Do you need Shopify Plus to use Functions?
For a custom build, yes. Shopify documents that only stores on a Shopify Plus plan can use custom apps that contain Shopify Function APIs. Public apps distributed through the App Store are available on all plans, so a non-Plus store can benefit from Functions written by someone else but cannot commission its own.
Can a Shopify Function call an external API?
Shopify notes that some functions support network access, and points to metafields on products and customers, or data passed as cart attributes, as the way to pre-populate what a function needs. The reliable design is to compute anything external upstream and make it available as input rather than assuming a call can be made at execution time.
Why can't a Shopify Function check the current date and time?
Because Shopify does not allow nondeterminism in functions, explicitly including randomising and clock functionality. A function given the same input must return the same output. Anything time-dependent — a dispatch cut-off, a promotion window, a delivery date — is computed outside the function and passed in through a metafield or a cart attribute.
What are the size and performance limits on Shopify Functions?
For carts up to 200 line items Shopify documents 11 million execution instructions, 128 kB of input and 20 kB of output, with a 256 kB compiled binary, 10,000 kB of linear memory and 512 kB of stack. The input query is limited to 3000 bytes with a maximum cost of 30. The output cap is the one most often hit, and Shopify notes it does not support bulk price transformations across all line items.
Should Functions be written in Rust or JavaScript?
Shopify provides templates and libraries for both and says any language that compiles to WebAssembly will work, but strongly recommends Rust as the most performant choice to avoid functions failing with large carts. For simple logic on small carts JavaScript is workable; for anything looping over a wholesale-sized cart, take the recommendation.
NEXT STEP
Free store audit
A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritised list of fixes.
