Clicks & Carts

Shopify Functions: custom discount, shipping and payment logic

Server-side business logic that runs inside Shopify's own infrastructure. What they can do, what they cannot, and where they replaced Scripts.

8 min read · Apps & checkout ·

Shopify Functions let you run your own logic inside Shopify's infrastructure at specific decision points — how a discount is calculated, which delivery options appear, which payment methods are offered, whether a cart is valid. They're compiled to WebAssembly, run server-side on Shopify's platform, and execute in milliseconds.

They are the supported replacement for Shopify Scripts, and the mechanism behind most of what checkout extensibility makes possible.

What they replaced, and why it matters

Shopify Scripts were Ruby, Plus-only, and ran at checkout with fairly free rein. Functions are more constrained by design: no network access, no arbitrary runtime, strict execution limits. In exchange they're fast, safe, versioned with your app, and they don't break when Shopify updates checkout.

If you're still running Scripts, migrating is not optional — and it's a rewrite rather than a port, because the execution model is different.

The function types

TypeDecides
Product discountWhich products get a discount and how much
Order discountDiscounts applied to the order total
Shipping discountDiscounts on delivery rates
Delivery customisationWhich delivery options appear, in what order, renamed or hidden
Payment customisationWhich payment methods appear, hidden or reordered
Cart and checkout validationWhether a cart can proceed, with an error message
Cart transformBundling and line-item merging
Fulfilment constraintsHow orders can be split and fulfilled

Between them these cover most of what merchants used to buy three apps to approximate.

How one works

A function receives an input from Shopify, and returns operations. You declare what input you need in GraphQL, so Shopify only computes what you use:

``graphql query Input { cart { lines { quantity merchandise { ... on ProductVariant { id product { hasTags(tags: ["trade"]) { hasTag } } } } } buyerIdentity { customer { hasTags(tags: ["wholesale"]) { hasTag } } } } } ``

Your code — Rust or JavaScript, compiled to Wasm — reads that input and returns a list of operations: apply this discount, hide that delivery option, reject this cart with this message.

The logic is pure. Same input, same output, no side effects.

The constraints

These are the ones that shape design:

  • No network access. A function cannot call your API, your ERP or anything else. Everything it needs must be in the input.
  • Strict execution limits on time and instruction count. Keep logic tight.
  • Limited input. You get what the input query can express, not arbitrary store data.

The consequence: any data your logic depends on has to be somewhere the function can read it — usually metafields on the product, customer or shop. So "apply this customer's negotiated rate" means an app writes the rate to a customer metafield, and the function reads it.

That split is the architecture of nearly every real Functions project: an app syncs data in, a function makes decisions from it — often with an admin UI extension so someone can see what it decided.

When to use one

  • Discount logic the native discount engine can't express — tiered pricing, conditional bundles, customer-group rates.
  • Delivery rules — hiding express shipping for hazardous or oversized items, renaming options, reordering by cost.
  • Payment rules — hiding a method above an order value, or for a specific customer group.
  • Cart validation — minimum order values, restricted combinations, per-customer purchase limits.

If you're currently doing any of these with a theme-level script or a stack of apps, a function is usually smaller, faster and more reliable.

When not to

  • The native feature already does it. Shopify's discount engine covers a lot; check before you build.
  • You need live external data at checkout. Functions can't call out. Either pre-compute it into a metafield or accept the constraint.
  • The logic is genuinely enormous. Execution limits are real.

Deploying

Functions ship as part of an app — including a custom app built for one merchant, which is a common and entirely reasonable pattern:

``bash npm run shopify app generate extension # choose a function type npm run deploy ``

Then the merchant activates it, usually by creating a discount or customisation that references it. Availability of some function types depends on the plan, so check that before promising it.

Testing

Functions are pure, which makes them unusually testable. Write unit tests against sample inputs covering the edge cases — empty cart, customer with no tags, a product missing the metafield your logic expects. That last one is where real functions break: the data wasn't there, and the function had no network to go and get it.

A function is a small pure decision. All the difficulty moves into making sure the data it needs is already sitting where it can see it.

Is this the problem you’re looking at?

Send me the link to your store and a line about what is going wrong. You get a straight answer within one business day — no pitch, no obligation.

[email protected]

Or see what I do around Shopify: services, work beyond the theme, selected work.

Keep reading

← All articles