Shopify Functions represent the most significant architectural shift in Shopify’s customisation model since the introduction of the Storefront API. They allow developers to write custom backend logic that runs directly on Shopify’s infrastructure — modifying how discounts are calculated, how delivery options are presented, how payments are processed, and how the cart and checkout are validated.

Functions replace the deprecated Script Editor (which was limited to Shopify Plus and a proprietary Ruby dialect) with a more powerful, more portable, and more performant system based on WebAssembly. If you have been building custom Shopify Plus customisations, Functions are where that capability is heading.

This guide covers the technical implementation of Shopify Functions — from project setup through to production deployment. We build custom Functions as part of our Shopify development work. This is particularly relevant if you are working with checkout extensibility or building custom checkout experiences.

What Shopify Functions are

Functions are small programs that run in a sandboxed WebAssembly environment on Shopify’s servers. They intercept specific points in the commerce pipeline and modify Shopify’s default behaviour. Unlike Liquid, which only controls presentation, Functions modify the actual business logic — prices, available shipping methods, payment options, and validation rules.

The execution model is straightforward. Shopify triggers the Function at the appropriate point (during discount calculation, for example). The Function receives an input payload containing relevant data (cart contents, customer information, metafield values). The Function processes the input and returns an output that modifies Shopify’s default behaviour. Shopify applies the Function’s output to the commerce pipeline.

Functions have strict constraints by design. They cannot make network requests, access the filesystem, or use any I/O operations. They receive input, process it in memory, and return output. All external data must be provided through the input query — typically via metafields on products, customers, or the shop. This ensures predictable, fast execution that does not create dependencies on external systems.

Functions are not general-purpose code execution. They are purpose-built extension points with strict input/output contracts, performance limits, and no external I/O. This is by design — it guarantees Functions cannot slow down the checkout or create data dependencies on external systems.

Diagram showing how Shopify Functions intercept the commerce pipeline at specific extension points
Functions intercept Shopify’s commerce pipeline at defined extension points, allowing custom logic without modifying Shopify’s core infrastructure.

Function types and extension points

Shopify provides several Function extension points, each targeting a specific part of the commerce pipeline:

  • Discount Functions — custom discount logic beyond Shopify’s built-in discounts. Supports product discounts, order discounts, and delivery discounts.
  • Delivery Customisation — rename, reorder, or hide shipping methods based on cart contents, customer attributes, or metafield values.
  • Payment Customisation — rename, reorder, or hide payment methods at checkout based on order amount, customer type, or product attributes.
  • Cart and Checkout Validation — enforce custom rules preventing checkout completion (minimum order values, quantity limits, geographic restrictions).
  • Cart Transform — modify cart line items (merge, split, add properties) before checkout.
  • Fulfilment Constraints — define rules for which items can be fulfilled together or from which locations.

Building discount functions

Discount Functions are the most common use case. They enable pricing logic that Shopify’s native discount system cannot handle — tiered volume discounts, buy-X-get-Y across specific collections, customer-group-specific pricing, and complex bundling rules.

Project setup

# Create a new Shopify app with a Function extension
npx @shopify/create-app@latest my-discount-app
cd my-discount-app

# Generate a Function extension
npx shopify app generate extension --template product_discounts --name volume-discount

# Project structure:
# extensions/volume-discount/
#   src/run.js          - Function logic
#   input.graphql       - Input query definition
#   shopify.extension.toml  - Extension configuration

Input query

# extensions/volume-discount/input.graphql
query RunInput {
  cart {
    lines {
      id
      quantity
      merchandise {
        ... on ProductVariant {
          id
          product {
            id
            title
            hasAnyTag(tags: ["volume-discount-eligible"])
          }
        }
      }
      cost {
        amountPerQuantity {
          amount
          currencyCode
        }
      }
    }
  }
  discountNode {
    metafield(namespace: "volume-discount", key: "config") {
      value
    }
  }
}

Function logic

// extensions/volume-discount/src/run.js
export function run(input) {
  const config = JSON.parse(
    input.discountNode.metafield?.value ?? '{}'
  );

  const tiers = config.tiers || [
    { minQuantity: 3, percentage: 10 },
    { minQuantity: 6, percentage: 20 },
    { minQuantity: 12, percentage: 30 },
  ];

  const targets = [];

  for (const line of input.cart.lines) {
    const variant = line.merchandise;
    if (variant.__typename !== 'ProductVariant') continue;
    if (!variant.product.hasAnyTag) continue;

    const applicableTier = tiers
      .filter(t => line.quantity >= t.minQuantity)
      .sort((a, b) => b.minQuantity - a.minQuantity)[0];

    if (applicableTier) {
      targets.push({
        productVariant: { id: variant.id },
        percentage: applicableTier.percentage
      });
    }
  }

  if (targets.length === 0) {
    return { discountApplicationStrategy: 'FIRST', discounts: [] };
  }

  return {
    discountApplicationStrategy: 'FIRST',
    discounts: [{
      message: 'Volume discount applied',
      targets: targets.map(t => ({ productVariant: { id: t.productVariant.id } })),
      value: {
        percentage: { value: targets[0].percentage.toString() }
      }
    }]
  };
}
Shopify Functions volume discount logic showing tiered pricing
Volume discount Functions enable tiered pricing that goes beyond Shopify’s built-in discount capabilities, with configuration stored in metafields for merchant control.

Delivery customisation functions

Delivery customisation Functions modify shipping options presented at checkout. They cannot create new shipping options but can hide, rename, or reorder existing ones.

// Hide express shipping for oversized items
export function run(input) {
  const operations = [];
  const hasOversizedItem = input.cart.lines.some(line =>
    line.merchandise.__typename === 'ProductVariant' &&
    line.merchandise.product.hasAnyTag // tag: "oversized"
  );

  if (hasOversizedItem) {
    for (const group of input.cart.deliveryGroups) {
      for (const option of group.deliveryOptions) {
        if (option.title.toLowerCase().includes('express') ||
            option.title.toLowerCase().includes('next day')) {
          operations.push({ hide: { deliveryOptionHandle: option.handle } });
        }
      }
    }
  }
  return { operations };
}

Payment customisation functions

Payment customisation controls which payment methods are visible at checkout. This is essential for B2B stores showing different options to different customer segments.

// Show invoice payment only for B2B customers, hide BNPL for high-value orders
export function run(input) {
  const operations = [];
  const isB2B = input.cart.buyerIdentity?.customer?.hasAnyTag;
  const total = parseFloat(input.cart.cost.totalAmount.amount);

  for (const method of input.paymentMethods) {
    if (method.name.includes('Invoice') && !isB2B) {
      operations.push({ hide: { paymentMethodHandle: method.handle } });
    }
    if (method.name.includes('Pay in') && total > 2000) {
      operations.push({ hide: { paymentMethodHandle: method.handle } });
    }
  }
  return { operations };
}

Cart and checkout validation

Validation Functions enforce business rules that prevent checkout completion. They return error messages displayed in the cart or checkout UI, preventing invalid orders from being placed.

// Minimum order value per product category
export function run(input) {
  const errors = [];
  const categoryTotals = {};

  for (const line of input.cart.lines) {
    const variant = line.merchandise;
    if (variant.__typename !== 'ProductVariant') continue;
    const type = variant.product.productType || 'General';
    categoryTotals[type] = (categoryTotals[type] || 0) + parseFloat(line.cost.totalAmount.amount);
  }

  const config = JSON.parse(input.validation.metafield?.value ?? '{}');
  for (const [category, minimum] of Object.entries(config.minimums || {})) {
    if (categoryTotals[category] && categoryTotals[category] < minimum) {
      errors.push({
        localizedMessage: `Minimum order for ${category} is £${minimum}. Current: £${categoryTotals[category].toFixed(2)}.`,
        target: "cart",
      });
    }
  }
  return { errors };
}
Cart validation Function preventing checkout when business rules are not met
Validation Functions enforce business rules at the cart level, displaying clear error messages when conditions are not met.

Development workflow and CLI

The Shopify CLI provides the complete development workflow for Functions. The typical cycle is write code, build (compile to Wasm), test with sample input, deploy, and configure through the admin.

# Start development mode
npx shopify app dev

# Build the Function (compiles to Wasm)
npx shopify app function build --path extensions/volume-discount

# Run the Function locally with test input
npx shopify app function run --path extensions/volume-discount

# Deploy to Shopify
npx shopify app deploy

During development, use shopify app function typegen to generate TypeScript types from your input.graphql query. This provides autocomplete and type checking for the input payload, catching errors before deployment.

WebAssembly compilation and performance

Functions compile to WebAssembly (Wasm) for execution on Shopify’s servers. The performance constraints are strict and non-negotiable:

  • Execution time: maximum 11ms (5ms instruction limit for some Function types)
  • Memory: maximum 10MB linear memory
  • Binary size: maximum 256KB Wasm binary
  • No I/O: no network requests, no file system, no timers, no randomness

JavaScript Functions compile through the Javy runtime, which adds overhead compared to Rust. For most use cases, JavaScript performance is adequate. For extremely complex logic processing large carts (100+ line items with multiple discount tiers), Rust provides better performance within the same limits. We recommend starting with JavaScript and moving to Rust only if you hit performance constraints.

Testing strategies

Test Functions with representative input data. Create JSON fixture files that mirror the actual input payloads Shopify will send.

// tests/fixtures/volume-discount-input.json
{
  "cart": {
    "lines": [{
      "id": "gid://shopify/CartLine/1",
      "quantity": 5,
      "merchandise": {
        "__typename": "ProductVariant",
        "id": "gid://shopify/ProductVariant/123",
        "product": {
          "id": "gid://shopify/Product/456",
          "title": "Test Product",
          "hasAnyTag": true
        }
      },
      "cost": { "amountPerQuantity": { "amount": "29.99", "currencyCode": "GBP" } }
    }]
  },
  "discountNode": {
    "metafield": { "value": "{\"tiers\":[{\"minQuantity\":3,\"percentage\":10}]}" }
  }
}

// Run: npx shopify app function run --path extensions/volume-discount < tests/fixtures/input.json

Also test edge cases: empty carts, carts with no eligible products, carts with products missing metafields, very large carts, and carts with mixed eligible and ineligible items.

Shopify Functions testing workflow with fixtures and local execution
Test Functions locally with JSON fixtures before deploying. Cover edge cases including empty carts, missing data, and large cart sizes.

Production deployment and monitoring

Deploying Functions to production requires creating the app, deploying the Function, and then activating it through the Shopify admin or API.

  • Deploy via CLI: npx shopify app deploy pushes compiled Wasm to Shopify.
  • Activate via admin: for discount Functions, create an automatic discount in the admin and select your custom Function as the discount type.
  • Monitor execution: use the Partner Dashboard to view execution logs, error rates, and performance metrics.
  • Version management: each deployment creates a new version. Shopify uses the latest deployed version automatically.
  • Configuration via metafields: store Function configuration (discount tiers, rules, thresholds) in shop or discount metafields so merchants can adjust without code changes.

Production monitoring checklist

  • Monitor error rates in the Partner Dashboard — any increase after deployment needs immediate investigation.
  • Track execution time — ensure you stay well within the 11ms limit to avoid intermittent failures.
  • Test the Function on the live store after deployment — verify discounts, shipping options, or validation rules work as expected.
  • Have a rollback plan — know how to quickly revert to the previous version if issues arise.

Shopify Functions represent the future of Shopify customisation. They provide a secure, performant, and scalable mechanism for modifying Shopify’s core business logic. The learning curve is steeper than Liquid, but the capabilities are significantly more powerful. For any Shopify Plus store with custom pricing, shipping, or validation requirements, Functions are the correct tool.

If you need custom Functions built for your Shopify store, get in touch. We build custom Functions as part of our Shopify development services.