Skip to content

When a Shopify Function is the right tool

A Function runs inside Shopify's checkout, which is both its power and its cage. What each target can do, what really needs Plus, and when to build something else.

GuideJune 17, 20268 min read

A Shopify Function is a small WebAssembly module you hand to Shopify. Shopify runs it on its own infrastructure at the exact moment it prices a cart, validates a checkout, or assembles the list of delivery options a buyer sees. Your server is not involved. For all but a narrow exception — a fetch target, limited to custom apps on Enterprise stores with network access granted — there is no HTTP call, no latency budget, and no outage of yours that can stop a purchase.

That single property — it runs where the decision is made — is the whole reason to reach for one. It is also the reason a Function is the wrong answer more often than people expect. The place where you can change the price is a place where you cannot call your database, cannot read the clock, and get eleven million instructions to make up your mind.

What follows is the decision worth writing down before anyone opens a terminal: what a Function is made of, what each target is allowed to do, which ones genuinely require Plus, and when a plain app or a theme change is the better tool.

What a Function is made of

Three parts, and being exact about them explains most of the limits people run into later.

An input query. A GraphQL document you write that declares the data your logic needs: cart lines, quantities, the customer's tags, whether a product sits in a given collection, a metafield holding configuration. Shopify runs it and hands you the result as a plain object. There is no second round trip. Whatever the input query did not ask for does not exist as far as your code is concerned.

A run target. One entry point, one input, one return value. Rust compiled to WebAssembly is the best-supported path and the one we reach for; JavaScript works and is easier to hire for. That choice affects your build tooling, not what the Function is permitted to do.

A list of operations. Your Function mutates nothing. It returns operations — expand this line, hide that payment method, add this product discount — and Shopify applies them, or rejects them as invalid. You are writing a pure function from cart to intent, which is why this code is unusually pleasant to unit test.

The resource limits are published and they bite. For carts up to two hundred lines: eleven million instructions per execution, 128 kB of input, 20 kB of output. Across all Functions: a 256 kB compiled binary, 10,000 kB of linear memory, and one kilobyte of logs before truncation. Shopify also forbids nondeterminism outright — no randomness, and no clock you can read. Time is not entirely closed off: several Function APIs expose a localTime object whose timeBefore, timeAfter, timeBetween and dateTimeBetween predicates answer questions about the current moment without ever handing you a timestamp. So "free shipping on Fridays" is expressible; "stamp the order with the time it was priced" is not.

Scroll the figure sideways to see all of it

The targets, and what each is allowed to do

Discounts

The Discount Function API splits across two run targets. The cart-lines target returns operations for the PRODUCT and ORDER discount classes; the delivery-options target returns the SHIPPING class. Which targets execute at all is gated by the discountClasses field on the discount the merchant created, so a Function that returns product discounts for a discount configured as shipping-only will do nothing, silently. That is the first thing to check when a discount "does not apply".

This is the target for anything the native discount types cannot express: tiered thresholds, conditional stacking, pricing that depends on a customer tag, buy-X-get-Y with an exclusion list.

Cart Transform

Three operations, and the differences matter more than the names suggest. lineExpand turns one cart line into several — how you add a fee line, a gift wrap, or the components of a bundle. linesMerge collapses several lines into one parent variant, the same idea in the other direction. lineUpdate changes a line in place.

Cart Transform is where an added fee lives, and the target we used for a configurable import fee with product and gift-card exclusions. It is also where the quantity bug further down comes from.

Delivery and payment customization

These hide, reorder and rename options the buyer is offered. They do not create options and they do not price them: a delivery customization cannot invent a shipping rate your shipping settings do not already produce. Payment customization has hard edges worth knowing before you promise anything — you cannot rename a method whose name is a logo, such as Shop Pay or Apple Pay, and these Functions do not run in Point of Sale at all. Creating options rather than filtering them is a different target again: the local pickup and pickup point generators.

Cart and checkout validation

The one that refuses. A validation Function blocks progress when the cart does not meet a rule — restricted regions, order minimums, incompatible combinations, a per-customer purchase limit. Because it runs on Shopify's servers rather than in the buyer's browser, it cannot be bypassed by someone who opens devtools or posts straight to the cart API. That is the entire point: if a rule matters commercially, it belongs here rather than in the theme.

Scroll the figure sideways to see all of it

What actually requires Plus

The usual summary — "Functions are Plus-only" — is wrong in a way that costs people real scoping decisions. The precise rule is about how the Function is delivered, not about what it does.

A store on any plan can install a public app from the Shopify App Store that contains Functions, and those Functions run. Only stores on Shopify Plus can install a custom app that contains Function APIs. So the plan question is really a distribution question. Building for one merchant on a custom app means they need Plus. Building something you will list publicly means their plan stops being the constraint and App Store review starts being one.

On top of that there are capability-level gates. The lineUpdate operation in Cart Transform is rejected unless the shop is on a Plus plan or a development store. Checkout UI extensions on the information, shipping and payment steps are Plus-only, which matters as soon as a Function needs a companion interface. Check the specific target against the specific plan before scoping, not after — it is a five-minute check that prevents a quote you cannot honour.

The per-unit versus per-line trap

This is the most common way this work reaches a customer broken, and it is worth understanding even if you never write a line of Rust.

A cart line is not an item. It is a merchandise reference plus a quantity. When a Cart Transform expands a line, the price you supply is fixedPricePerUnit — per unit, not per line. Meanwhile the input hands you cost.amountPerQuantity, which is also per unit, and quantity sitting right next to it.

So a developer implementing "add a twelve dollar handling fee" has four plausible-looking arrangements of the same three numbers, and three of them are wrong. Two of those three look perfectly correct in a development store, because whoever tests it adds one of each product. The bug appears when a real customer buys eleven.

The fix is not cleverness. It is writing the rule in prose — "twelve dollars per unit, on eligible units only" — before writing the query, and then testing at a quantity that is not one. The long version is in building a cart transform that counts correctly.

Function, app, or theme change?

Three questions, in order.

Does the rule have to hold at checkout, even for a buyer trying to get around it? If yes, it is a Function. Theme JavaScript and cart-page logic are advisory: they can be skipped, and a rule that can be skipped is not a rule. This question settles most cases on its own.

Does the logic need something Shopify cannot hand it in the input? A live inventory figure from your ERP, a fraud score, a credit limit held in an external system. Functions have no general network access, so the answer is an app that syncs that value into a metafield on a schedule, and then, if the rule needs enforcing, a Function that reads the metafield. Two pieces, not one.

Is it presentation? A badge, an explanation, a delivery-date picker, a message about why an item is excluded. That is a theme change or a checkout UI extension. A Function renders nothing, ever. It decides, and the buyer sees only the result.

The honest failure mode here is reaching for a Function because it is the interesting thing to build. A good share of the requests that arrive labelled "we need a Function" turn out to be a discount the native types already cover, or a message that belongs on the product page.

Where to start

Write the rule as prose a merchant would agree to, including what it excludes and what happens at quantity two. Then pick the target. Then check the plan against that target. Then build it, with the configuration in metafields so the numbers can change without a deploy.

In that order this is a small, testable, durable piece of software. Out of that order it is a fee that charged eleven customers the wrong amount before anybody noticed. Our Shopify Functions development page describes the sequence end to end.

Blog