Services
Store Setup UX/UI Redesign Migration Custom Development Shopify SEO Speed & Performance
Work Themes Apps
Tools
All Tools SEO Audit ROI Calculator Migration Cost Store Speed Test Theme Detector Migration Readiness Homepage CRO Review
Resources
Resource Library Blog Pricing
About Contact Free Audit ROI Calculator Migration Cost Store Speed Test
🇬🇧 EN 🇹🇷 TR 🇩🇪 DE
Shopify Tips

Shopify Functions — Custom Discounts, Shipping and Payment Logic: 2026 Developer Guide

Shopify Functions is the official successor to Shopify Scripts (deprecated 2024) — Wasm-based, executed at the edge, written in Rust or JavaScript. A complete developer guide with code samples, API walkthrough, and real migration experience.

15 min read
1 views

In August 2025, Shopify finally turned Shopify Scripts off. A Plus merchant from the Rhineland called us 48 hours later: his checkout was throwing errors on 60% of orders, three automated B2B discounts had stopped working, and his ops team was correcting orders by hand. The previous vendor had known for 18 months that Scripts were deprecated and done nothing. Three weeks of crisis mode later, we had four Shopify Functions live, the checkout was clean again, and the merchant had reclaimed 80 manual hours per month.

This story is not unusual in 2026. Shopify Functions is not optional — it is the only supported way to write server-side custom logic in a Shopify Plus store. Anyone in 2026 still believing that an admin discount code or a marketplace app is enough either has a very simple use case or is leaking margin to unnecessary manual work.

This guide is a complete technical reference for Shopify Plus developers, technical CTOs, and agencies evaluating Functions. We walk through all seven Function APIs, show real code in Rust and JavaScript, cover the build workflow with Shopify CLI, compare languages, document Scripts migration, and share a real case study with hard numbers.

What Shopify Functions is (and isn't)

Shopify Functions are WebAssembly modules executed at the Shopify edge to extend store business logic in four areas: discounts, shipping, payment, and cart transformation. They replace the Shopify Scripts API deprecated in 2024 and have been the only supported option for server-side customisation since summer 2024.

Conceptually, a Function runs like this: Shopify invokes your Wasm module with an input (typically the cart state plus context metadata), your code returns a list of operations (apply discount X, hide shipping method Y, split line item Z), and Shopify applies those operations on the storefront or checkout. There is no HTTP roundtrip in the traditional sense — the Function runs in the same edge data centre as the storefront, with typical latencies between 3 and 30ms.

The key properties:

  • Wasm-based: compiled to WebAssembly, sandboxed, deterministic
  • Languages: Rust (officially recommended, best performance) or JavaScript (via Javy, faster to write)
  • Size limit: 256KB Wasm bytecode per function
  • Latency budget: hard 5ms execution cap, with buffer for input/output serialization
  • No external API calls: the most important constraint — Functions are hermetically sealed
  • Deterministic: same input always returns same output (no date, no random numbers without workaround)

The seven Function APIs in 2026

As of May 2026, seven Function APIs are in production:

  1. Product Discount Function — discounts on individual line items or product variants
  2. Order Discount Function — discounts on the full cart subtotal
  3. Shipping Discount Function — discounts on shipping costs (e.g. free shipping)
  4. Delivery Customization Function — hide, rename, sort delivery methods
  5. Payment Customization Function — hide, rename, sort payment methods
  6. Cart Transform Function — expand line items (bundles), merge them, or present them differently
  7. Cart and Checkout Validation Function — block checkout when business rules are violated

When you actually need Functions (and when you don't)

Before building a Function, run through the decision matrix in this order:

  1. Native Shopify rule: can an admin discount code, standard shipping rule or Shopify Markets handle it? If yes, do that.
  2. Marketplace app: is there a proven app in the Shopify App Store (Discounty, Bold Discounts, Advanced Shipping Rules)? If yes and the licence cost is below 200 EUR/month: usually the right call.
  3. Custom Function: only when neither native nor app fits, build a Function.

Real scenarios where we recommend Functions:

  • Tiered B2B discounts: 5% from 1,000 EUR, 10% from 5,000 EUR, 15% from 10,000 EUR — combined with customer-tag-based multipliers. No app handles this cleanly.
  • Dynamic shipping rules: hide express shipping when cart contains fragile items; force freight shipping above 50kg.
  • Region-specific payment hides: invoice only for verified B2B customers in DACH; hide Klarna in countries with high cancellation rates.
  • Bundle logic: a purchased bundle expands into three line items for inventory, but stays as one item in the customer-facing cart.
  • MOQ validation: block checkout when a B2B customer drops below the minimum order quantity.

The seven Function APIs in detail

1. Product Discount Function

This Function returns a list of discount operations on individual line items. Ideal for use cases like „buy 2 from category X, get the third 30% off" or „member-only 15% on brand tag XYZ".

Input schema (truncated):

query Input {
  cart {
    lines {
      id
      quantity
      merchandise {
        ... on ProductVariant {
          id
          product {
            tags
            inAnyCollection(ids: ["gid://shopify/Collection/123"])
          }
        }
      }
    }
    buyerIdentity {
      customer {
        hasAnyTag(tags: ["vip", "wholesale"])
      }
    }
  }
  discountNode {
    metafield(namespace: "functions", key: "config") {
      value
    }
  }
}

Rust implementation for „buy 2 from collection 123, third 30% off":

use shopify_function::prelude::*;
use shopify_function::Result;

#[shopify_function_target(query_path = "src/run.graphql", schema_path = "schema.graphql")]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
    let mut eligible_lines: Vec<_> = input.cart.lines.iter()
        .filter(|line| matches!(
            &line.merchandise,
            input::InputCartLinesMerchandise::ProductVariant(v)
                if v.product.in_any_collection
        ))
        .collect();

    if eligible_lines.len() < 3 {
        return Ok(output::FunctionRunResult { discounts: vec![], discount_application_strategy: output::DiscountApplicationStrategy::FIRST });
    }

    eligible_lines.sort_by(|a, b| a.quantity.cmp(&b.quantity));
    let target_line = eligible_lines.first().unwrap();

    Ok(output::FunctionRunResult {
        discounts: vec![output::Discount {
            message: Some("Buy 2 get 30% off third".into()),
            targets: vec![output::Target::ProductVariant(
                output::ProductVariantTarget {
                    id: target_line.merchandise_variant_id().to_string(),
                    quantity: Some(1),
                },
            )],
            value: output::Value::Percentage(output::Percentage { value: 30.0.into() }),
        }],
        discount_application_strategy: output::DiscountApplicationStrategy::FIRST,
    })
}

The JavaScript equivalent of the same logic:

// @ts-check
export function run(input) {
  const eligible = input.cart.lines.filter(
    (line) => line.merchandise.__typename === "ProductVariant"
      && line.merchandise.product.inAnyCollection
  );

  if (eligible.length < 3) {
    return { discounts: [], discountApplicationStrategy: "FIRST" };
  }

  eligible.sort((a, b) => a.quantity - b.quantity);
  const target = eligible[0];

  return {
    discounts: [{
      message: "Buy 2 get 30% off third",
      targets: [{ productVariant: { id: target.merchandise.id, quantity: 1 } }],
      value: { percentage: { value: 30.0 } }
    }],
    discountApplicationStrategy: "FIRST"
  };
}

Gotchas: discount-stacking logic is tricky — if multiple Functions try to discount the same line item, FIRST or MAXIMUM wins depending on strategy. Test edge cases with Function Runner.

2. Order Discount Function

Discounts on the full cart subtotal. Classic example: „from 200 EUR, 10% off for VIP customers".

#[shopify_function_target(query_path = "src/run.graphql", schema_path = "schema.graphql")]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
    let subtotal: f64 = input.cart.cost.subtotal_amount.amount.into();
    let is_vip = input.cart.buyer_identity
        .as_ref()
        .and_then(|bi| bi.customer.as_ref())
        .map(|c| c.has_any_tag)
        .unwrap_or(false);

    if subtotal < 200.0 || !is_vip {
        return Ok(output::FunctionRunResult { discounts: vec![], discount_application_strategy: output::DiscountApplicationStrategy::FIRST });
    }

    Ok(output::FunctionRunResult {
        discounts: vec![output::Discount {
            message: Some("VIP 10% over 200 EUR".into()),
            targets: vec![output::Target::OrderSubtotal(output::OrderSubtotalTarget {
                excluded_variant_ids: vec![],
            })],
            value: output::Value::Percentage(output::Percentage { value: 10.0.into() }),
        }],
        discount_application_strategy: output::DiscountApplicationStrategy::FIRST,
    })
}

3. Shipping Discount Function

Discounts on shipping costs — most often „free shipping above threshold X". Important: this only discounts an already-calculated shipping rate. To hide or rename shipping rates entirely, you need a Delivery Customization Function (next section).

export function run(input) {
  const subtotal = parseFloat(input.cart.cost.subtotalAmount.amount);
  const targets = input.cart.deliveryGroups.flatMap((group) =>
    group.deliveryOptions.map((option) => ({
      deliveryOption: { handle: option.handle }
    }))
  );

  if (subtotal < 75.0 || targets.length === 0) {
    return { discounts: [], discountApplicationStrategy: "FIRST" };
  }

  return {
    discounts: [{
      message: "Free shipping over 75 EUR",
      targets,
      value: { percentage: { value: 100.0 } }
    }],
    discountApplicationStrategy: "FIRST"
  };
}

4. Delivery Customization Function

This Function manipulates the list of displayed shipping methods. Three operations are possible: hide, rename, and move (reorder). Use case: hide express shipping when the cart contains items tagged „fragile".

export function run(input) {
  const hasFragile = input.cart.lines.some((line) =>
    line.merchandise.__typename === "ProductVariant"
      && line.merchandise.product.hasAnyTag
  );

  if (!hasFragile) {
    return { operations: [] };
  }

  const operations = input.cart.deliveryGroups.flatMap((group) =>
    group.deliveryOptions
      .filter((option) => option.title.toLowerCase().includes("express"))
      .map((option) => ({
        hide: { deliveryOptionHandle: option.handle }
      }))
  );

  return { operations };
}

5. Payment Customization Function

Identical to Delivery Customization but for payment methods. Popular use case: show invoice payment only to B2B customers tagged „credit-approved".

export function run(input) {
  const isCreditApproved = input.cart.buyerIdentity?.customer?.hasAnyTag === true;

  if (isCreditApproved) {
    return { operations: [] };
  }

  const operations = input.paymentMethods
    .filter((method) => method.name.toLowerCase().includes("invoice")
      || method.name.toLowerCase().includes("rechnung"))
    .map((method) => ({
      hide: { paymentMethodId: method.id }
    }));

  return { operations };
}

6. Cart Transform Function

This Function is more powerful and more complex. It can expand line items (a bundle becomes three separate items for inventory) or merge them (three items become one bundle for display). It needs Cart-Transform-specific metafields to define bundle components.

Use case: a starter kit product contains three components. The customer sees one item in the storefront; the order arrives as three line items so the WMS can pick each component correctly.

export function run(input) {
  const operations = input.cart.lines
    .filter((line) => line.merchandise.__typename === "ProductVariant"
      && line.merchandise.bundleComponents?.value)
    .map((line) => {
      const components = JSON.parse(line.merchandise.bundleComponents.value);
      return {
        expand: {
          cartLineId: line.id,
          expandedCartItems: components.map((c) => ({
            merchandiseId: c.variantId,
            quantity: c.quantity * line.quantity,
            price: { adjustment: { fixedPricePerUnit: { amount: c.price } } }
          }))
        }
      };
    });

  return { operations };
}

7. Cart and Checkout Validation Function

Blocks checkout when business rules are violated. Example: B2B customer attempting to order below MOQ, or mixed incompatible products in cart.

export function run(input) {
  const errors = [];
  const moq = 50;
  const totalQty = input.cart.lines.reduce((sum, line) => sum + line.quantity, 0);
  const isB2B = input.cart.buyerIdentity?.customer?.hasAnyTag === true;

  if (isB2B && totalQty < moq) {
    errors.push({
      localizedMessage: `B2B minimum order is ${moq} units. Current cart: ${totalQty}.`,
      target: "$.cart"
    });
  }

  return { errors };
}

Build and deploy workflow

The full Functions workflow runs on Shopify CLI 3.x. One-time setup per machine:

npm install -g @shopify/cli @shopify/app
shopify version  # should be 3.50.0 or newer
shopify app init my-functions-app --template=remix
cd my-functions-app
shopify app generate extension --type=product_discounts --name=tiered-b2b-discount

The Function is generated under extensions/tiered-b2b-discount/ with src/run.ts or src/lib.rs, a GraphQL query file, and the Shopify config. Local testing with Function Runner:

cat test-input.json   # contains: { "cart": { "lines": [...] } }
shopify app function run --path=extensions/tiered-b2b-discount < test-input.json

Deployment:

shopify app deploy

Each deploy creates a new version. In the Shopify admin you can switch between versions — important for rollback on production bugs. Versions are numbered through the Shopify Partners API; in 2026 the versions API is stable and supports per-Function version pinning.

Rust vs JavaScript — which language to pick

Both languages compile to Wasm, both are officially supported, both run at the same edge. But the trade-offs are real:

DimensionRustJavaScript (Javy)
Cold latency3-5ms10-30ms
Bytecode size30-100KB typical80-220KB typical
Learning curvesteep (borrow checker, lifetimes)flat (modern ES6/TS)
Write speed2-3x slowerbaseline
Type safetycompile-time guaranteedonly with TypeScript
JSON handlingserde, somewhat verbosenative, easy
Wasm toolchaincargo + wasm-pack matureJavy stable since 2024

Our recommendation in 2026:

  • JavaScript for: simple discount logic, fast prototypes, teams without Rust experience, Functions under 50 lines of logic
  • Rust for: compute-heavy validation Functions, Cart Transform with large bundle hierarchies, complex tiered discount engines, Functions getting close to the 256KB limit

Practical rule of thumb: 70% of our Functions ship in JavaScript, 30% in Rust. The performance difference is real but for most use cases below the threshold of human perception.

Migrating from Shopify Scripts to Functions

Anyone not migrated by 2026 has a problem — Scripts were fully disabled in August 2025. The migration is not 1:1, because the programming model is fundamentally different.

Old Script typeNew Function APIMigration difficulty
Line Item ScriptProduct Discount Functioneasy
Shipping ScriptDelivery Customization + Shipping Discountmedium (two Functions needed)
Payment ScriptPayment Customization Functioneasy
Bundle logic (informal)Cart Transform Functionhard (new data modelling)
MOQ validationCart and Checkout Validation Functioneasy

Common migration pitfalls:

  1. External API calls: Scripts sometimes had workarounds for external calls. Functions cannot. You must migrate that data into the cart-metafield model.
  2. Customer-tag logic: tags are now explicitly queryable via hasAnyTag. Anyone parsing customer notes before must move that to tags.
  3. Discount stacking: Functions have an explicit discountApplicationStrategy. Verify whether FIRST, MAXIMUM or ALL matches your old logic.
  4. Test coverage: Scripts were tested in the live checkout. Functions need Function Runner and ideally an automated test suite.

Case study — Plus merchant recovers 180K EUR margin with 4 Functions

A Shopify Plus merchant from North Rhine-Westphalia, anonymous on request, runs a B2B + DTC hybrid with around 4.2M EUR annual revenue. Situation in February 2026:

  • Scripts disabled since August 2025, stopgap via manual discount codes
  • Ops team spending 80 hours/month on manual corrections
  • Discount errors costing 12-18K EUR/month in over-granted discounts
  • B2B customers complaining about missing tier discounts
  • Express shipping offered for fragile glass items — 7% breakage rate

Over 8 weeks we built four Functions:

  1. Tiered B2B Order Discount Function (Rust) — four tier levels, combined with customer-tag multiplier for top accounts
  2. Fragile Items Delivery Customization Function (JavaScript) — hides express shipping when „fragile" tag is in cart
  3. B2B Invoice Payment Customization Function (JavaScript) — invoice only for credit-approved customers
  4. Bundle Cart Transform Function (Rust) — three starter kits expand into components for WMS picking

Result after three months in production:

  • Discount errors: 14K EUR/month → 200 EUR/month
  • Manual ops hours: 80/month → 6/month
  • Glass-item breakage rate: 7% → 0.4%
  • Average B2B order value: +18% from correctly applied tier discounts
  • Annualised effect: roughly 180K EUR margin recovered

Investment: 24K EUR implementation, 800 EUR/month maintenance retainer. ROI hit in month 2.

Our typical Functions delivery process

Week 1: Discovery

We map all existing custom-logic requirements, extract legacy Scripts (if still around), interview ops and sales on manual processes, identify data sources (customer tags, metafields, collections). Output: a document with every planned Function, input data, and success criteria.

Week 2: Architecture

Language choice per Function (Rust vs JavaScript), metafield schema design, GraphQL query modelling, test strategy, deploy pipeline. We scaffold the app skeleton with Shopify CLI and commit the repo.

Weeks 3-7: Implementation

Per Function: skeleton, GraphQL query, logic, unit tests, Function Runner integration tests. Four Functions typically take 3-5 weeks, depending on complexity.

Week 8: Testing & deployment

Full end-to-end tests in dev store, performance profiling, production deploy with version pinning, A/B test phase over two weeks for critical Functions.

Ongoing: handover

Documentation, monitoring setup via Shopify Functions Logs API, maintenance retainer for API version updates and small logic adjustments.

Best Shopify apps and tools for Functions development in 2026

ToolPurposeWhen to use
Shopify CLI 3.xFunction scaffolding, local testing, deployMandatory — base tool for every Function project
Function RunnerLocal Function execution with test inputsDuring development, before every deploy
GraphQL Admin API ExplorerQuery development, schema inspectionEach new Function for input validation
Shopify Functions API ReferenceOfficial schema documentationLookup for output shapes and strategies
App BridgeAdmin UI configuration for FunctionsWhen merchants must self-configure Function parameters
Polaris ComponentsUI library for admin settings panelsStandard for any configuration UI
Shopify FlowWorkflow orchestration around FunctionsWhen Function triggers are driven by other events
Klaviyo Server-Side EventsTrigger marketing events post-discountFor DTC brands with aggressive email automation
VS Code Shopify ExtensionLiquid + Functions syntax highlightingDX boost in any Shopify project
wasm-packRust-to-Wasm toolchainFor every Rust-based Function
Hydrogen StorefrontFrontend integration with FunctionsHeadless setups with custom cart UI
Postman / InsomniaAdmin API testing for metafield setupEvery Function with configuration metafields

Frequently asked questions

Do I need to know Rust to use Shopify Functions?

No. JavaScript via the Javy toolchain is officially supported and sufficient for the majority of Functions. Rust gives roughly 3-5x better cold latency and tighter bytecode, but the performance difference is barely perceptible for simple discount logic. We recommend JavaScript as default, Rust only for compute-heavy Cart Transform or Validation Functions.

Are Functions free in Shopify Plus or do they cost extra?

Functions themselves are included in every Shopify plan at no extra cost — no per-execution fees like Lambda. There are however per-merchant limits: maximum 5 Discount Functions, 1 Cart Transform Function, 5 Validation Functions per store (as of May 2026). Custom-app licence costs or agency hours for development are separate.

Can Functions call external APIs?

No, and this is the most important constraint. Functions are hermetically sealed — they run Wasm-sandboxed within a fixed latency budget. All data you need must come from the cart input, customer data, or metafields. If your logic needs external data, sync it into metafields via a background job (typically through Shopify Flow or a separate app).

What latency impact does this add to checkout?

Typically 3-5ms for Rust, 10-30ms for JavaScript. Compared to an 800ms checkout roundtrip, this is negligible. Shopify enforces a hard 5ms execution cap per Function — exceed it, and the Function is aborted and its operations ignored. Functions are parallelised where possible.

How do I test a Function before going live?

Three stages: (1) Function Runner with synthetic inputs during development. (2) Development store deploy for end-to-end tests in the real cart flow. (3) Staging store or a Plus branch store for pre-production tests with real payment methods. We recommend automated Function-Runner tests in the CI pipeline.

What happens to Functions when Shopify rolls a new API version?

Functions are explicitly bound to an API version (e.g. 2025-10). When Shopify rolls a new version, your Function keeps running on the old one until you actively migrate. Shopify typically grants a 12-month migration window per version. Version migration means: update the GraphQL query, adjust output shapes if needed, redeploy, archive the old version.

Conclusion — pulling it all together

Shopify Functions is no longer optional in 2026 — it is mandatory for every Plus merchant with non-trivial business logic. The old Scripts are dead, the app market only covers the most common 60% of cases, and anything that genuinely affects margin or ops efficiency runs through custom Functions. Anyone ignoring this either pays in manual labour or in margin lost to incorrectly applied discounts.

The good news: tooling is mature in 2026. Shopify CLI 3.x is stable, Function Runner offers a solid testing experience, JavaScript support has dramatically reduced the learning curve, and the GraphQL-based schemas make the programming model explicit and testable. Once you have one Function deployed successfully, the conceptual problem is gone — subsequent Functions ship faster.

If you are evaluating Functions, getting started, or migrating from Scripts, also read our related pillar posts: Shopify Hydrogen and Headless Commerce for frontend integration, Shopify Checkout Extensibility for checkout UI customisation, and Shopify Plus B2B Wholesale Portal for the most common Functions application in B2B. For a concrete discovery conversation contact our Shopify Plus agency in Korschenbroich — 20 minutes from Düsseldorf, English-speaking, with real Functions experience.

Share this article
Back to Blog

Related Posts

Shopify E-Commerce Logistics &amp; Fulfillment in Hamburg: The Complete Guide 2026
Hamburg is Germanys logistics capital — Europes second-largest port, headquar...
Shopify B2B Wholesale in Düsseldorf: The Complete Guide for Wholesale Stores 2026
Düsseldorf is Germanys B2B capital — trade-fair metropolis, Japanese Quarter,...
Shopify for Fashion Brands in Cologne: The Complete E-Commerce Guide 2026
Cologne is Germanys third-largest fashion market. This guide covers 42 concre...
34Devs Chat Assistant
34Devs Chat Assistant
34Devs Assistant
Online
Hey! What would you like to improve on your website?
Need a human? Just ask.