Skip to main content

Shopify Functions

Shopify Functions are the most powerful extension point in the Shopify platform. They let you run custom logic inside Shopify's infrastructure to customize core commerce operations: discounts, shipping rates, payment methods, cart transformations, order routing, and more. Functions execute in under 5 milliseconds as WebAssembly modules, making them fast enough to run on every checkout without impacting performance. This lesson covers the architecture, extension targets, development workflow, and the critical migration from deprecated Shopify Scripts.

What Are Shopify Functions?

Think of Functions as serverless micro-programs that intercept and modify Shopify's commerce logic. Unlike apps that run on your own servers and communicate via APIs, Functions run on Shopify's servers as sandboxed WebAssembly (Wasm) modules.

Why Functions Matter

  • No network latency: Your code runs on Shopify's servers, right next to the data
  • Guaranteed performance: Strict execution limits (5ms, 11MB memory) ensure checkout speed
  • Reliability: No external server to go down, no webhook delivery failures
  • Scale: Runs on every checkout regardless of traffic volume
  • Security: Sandboxed Wasm cannot access the network, filesystem, or other stores
Functions Replace Scripts

Shopify Scripts (Ruby-based checkout customization, previously Plus-only) are deprecated and will be removed in June 2026. All merchants using Scripts must migrate to Functions. If you maintain Scripts for clients, migration planning should start now.

Extension Targets

Each Function targets a specific part of the commerce flow. Here are the available targets:

Extension TargetWhat It CustomizesExample Use Case
purchase.product-discount.runProduct-level discounts"Buy 2, get 10% off"
purchase.order-discount.runOrder-level discounts"20% off orders over $100"
purchase.shipping-discount.runShipping discounts"Free shipping on orders over $50"
purchase.payment-customization.runPayment method visibilityHide COD for international orders
purchase.delivery-customization.runShipping option customizationRename, reorder, or hide shipping rates
cart-transform.runCart modificationsAuto-bundle products, merge line items
purchase.validation.runCart/checkout validationEnforce quantity limits, address rules
fulfillment-constraints.runFulfillment routingRoute orders to specific locations
order-routing.runOrder routing logicSend orders to nearest warehouse

Development Workflow

Creating a Function

Use the Shopify CLI to scaffold a new Function:

# Navigate to your app directory
cd ~/shopify-projects/masterclass-app

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

This creates a new directory in your extensions/ folder:

extensions/volume-discount/
├── src/
│ └── run.js # Function logic (or run.rs for Rust)
├── input.graphql # Input query definition
├── shopify.extension.toml # Extension configuration
└── package.json # Dependencies (JS only)

The Input Query

Every Function starts with a GraphQL input query that defines what data the Function receives. This runs before your Function executes and determines what information is available.

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

The input query runs on every checkout. Keep it minimal -- only request the data your Function actually needs. Unnecessary fields add latency and memory usage.

Writing Functions in JavaScript

JavaScript Functions use the @shopify/shopify_function package:

// extensions/volume-discount/src/run.js
import { DiscountApplicationStrategy } from "../generated/api";

/**
* Volume discount: Buy 3+ of an eligible product, get a percentage off.
*/
export function run(input) {
// Parse configuration from the metafield
const config = JSON.parse(
input?.discountNode?.metafield?.value ?? '{"quantity": 3, "percentage": 10}'
);

const targets = [];

for (const line of input.cart.lines) {
const variant = line.merchandise;

// Skip non-product lines (e.g., gift cards)
if (variant.__typename !== "ProductVariant") continue;

// Check if the product is eligible for volume discount
if (!variant.product.hasAnyTag) continue;

// Check if the quantity meets the threshold
if (line.quantity >= config.quantity) {
targets.push({
productVariant: {
id: variant.id,
},
});
}
}

// If no targets qualify, return empty discounts
if (targets.length === 0) {
return { discounts: [], discountApplicationStrategy: DiscountApplicationStrategy.First };
}

return {
discounts: [
{
value: {
percentage: {
value: String(config.percentage),
},
},
targets,
message: `${config.percentage}% off when you buy ${config.quantity}+`,
},
],
discountApplicationStrategy: DiscountApplicationStrategy.First,
};
}

Writing Functions in Rust

Rust Functions compile to smaller, faster Wasm modules. They are ideal when you need maximum performance or are comfortable with Rust:

// extensions/volume-discount/src/run.rs
use shopify_function::prelude::*;
use shopify_function::Result;

use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
struct Config {
quantity: i64,
percentage: f64,
}

impl Default for Config {
fn default() -> Self {
Config {
quantity: 3,
percentage: 10.0,
}
}
}

generate_types!(
query_path = "./input.graphql",
schema_path = "./schema.graphql"
);

#[shopify_function_target(query_path = "./input.graphql", schema_path = "./schema.graphql")]
fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
let config: Config = input
.discount_node
.metafield
.as_ref()
.and_then(|m| serde_json::from_str(&m.value).ok())
.unwrap_or_default();

let mut targets = vec![];

for line in &input.cart.lines {
if let input::InputCartLinesMerchandise::ProductVariant(variant) = &line.merchandise {
if variant.product.has_any_tag && line.quantity >= config.quantity {
targets.push(output::Target {
product_variant: Some(output::ProductVariantTarget {
id: variant.id.clone(),
}),
});
}
}
}

if targets.is_empty() {
return Ok(output::FunctionRunResult {
discounts: vec![],
discount_application_strategy:
output::DiscountApplicationStrategy::FIRST,
});
}

Ok(output::FunctionRunResult {
discounts: vec![output::Discount {
value: output::Value {
percentage: Some(output::Percentage {
value: config.percentage.to_string(),
}),
fixed_amount: None,
},
targets,
message: Some(format!(
"{}% off when you buy {}+",
config.percentage, config.quantity
)),
conditions: None,
}],
discount_application_strategy:
output::DiscountApplicationStrategy::FIRST,
})
}
JavaScript vs Rust

Choose JavaScript when: your team knows JS, the logic is straightforward, and you want fast iteration. JavaScript Functions are easier to write and debug.

Choose Rust when: you need the smallest Wasm binary, maximum execution speed, or you are comfortable with Rust. Rust Functions typically compile to 50-100KB vs 200-500KB for JavaScript.

Both compile to WebAssembly and run in the same sandbox. The performance difference is usually negligible for typical discount/shipping logic.

Extension Configuration

The shopify.extension.toml file configures your Function:

name = "volume-discount"
type = "product_discounts"
api_version = "2026-04"

[build]
command = "npm exec -- shopify app function build"
path = "dist/function.wasm"
watch = ["src/**/*.js"]

[ui]
enable_create = true

[ui.paths]
create = "/app/volume-discount/:functionId/create"
details = "/app/volume-discount/:functionId/:id"

The [ui] section is important -- it tells Shopify where merchants configure the discount in your app's UI. When a merchant creates a new discount using your Function, Shopify loads the URL specified in ui.paths.create inside your embedded app.

Testing Functions

Local Testing

Test Functions locally before deploying:

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

# Test with a specific input file
echo '{"cart":{"lines":[{"quantity":5,"merchandise":{"__typename":"ProductVariant","id":"gid://shopify/ProductVariant/1","product":{"id":"gid://shopify/Product/1","title":"Widget","hasAnyTag":true}},"cost":{"amountPerQuantity":{"amount":"10.00","currencyCode":"USD"}}}]},"discountNode":{"metafield":{"value":"{\"quantity\":3,\"percentage\":15}"}}}' | shopify app function run --path extensions/volume-discount

Unit Testing (JavaScript)

// extensions/volume-discount/src/run.test.js
import { describe, it, expect } from "vitest";
import { run } from "./run";

describe("volume-discount function", () => {
it("applies discount when quantity meets threshold", () => {
const input = {
cart: {
lines: [
{
quantity: 5,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: true },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: {
metafield: {
value: '{"quantity": 3, "percentage": 10}',
},
},
};

const result = run(input);

expect(result.discounts).toHaveLength(1);
expect(result.discounts[0].value.percentage.value).toBe("10");
expect(result.discounts[0].targets).toHaveLength(1);
});

it("returns no discounts when quantity is below threshold", () => {
const input = {
cart: {
lines: [
{
quantity: 1,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: true },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: {
metafield: {
value: '{"quantity": 3, "percentage": 10}',
},
},
};

const result = run(input);

expect(result.discounts).toHaveLength(0);
});

it("skips non-eligible products", () => {
const input = {
cart: {
lines: [
{
quantity: 5,
merchandise: {
__typename: "ProductVariant",
id: "gid://shopify/ProductVariant/1",
product: { hasAnyTag: false },
},
cost: {
amountPerQuantity: { amount: "10.00", currencyCode: "USD" },
},
},
],
},
discountNode: { metafield: null },
};

const result = run(input);

expect(result.discounts).toHaveLength(0);
});
});

Scripts to Functions Migration

Shopify Scripts (Ruby-based, Plus-only) are deprecated with a June 2026 removal deadline. If you or your clients use Scripts, migration is mandatory.

Migration Timeline

DateMilestone
2024Functions GA, Scripts deprecation announced
2025No new Scripts can be created
January 2026Migration tools available in admin
June 2026Scripts permanently removed

What Changes

Shopify ScriptsShopify Functions
Ruby languageJavaScript or Rust (compiled to Wasm)
Plus-onlyAvailable on all plans
Script Editor appBuilt into your app or standalone
Limited to line item, shipping, paymentMany more extension targets
No input queryExplicit GraphQL input query
Mutable cart objectImmutable input, declarative output

Migration Strategy

  1. Audit existing Scripts: List all Scripts in use, their type (line item, shipping, or payment), and their logic.
  2. Map to Function targets: Line item scripts map to product-discount or order-discount. Shipping scripts map to delivery-customization. Payment scripts map to payment-customization.
  3. Rewrite the logic: Translate Ruby logic to JavaScript or Rust. The paradigm is different -- Functions use immutable input and declarative output instead of mutating a cart object.
  4. Build a configuration UI: Scripts used the Script Editor; Functions need a merchant-facing UI in your app.
  5. Test thoroughly: Use the Function testing tools and verify against the same scenarios your Scripts handled.
  6. Deploy and activate: Install the Function on the store and deactivate the Script.
Do Not Wait Until June 2026

Migration takes time, especially for complex Scripts with many conditional rules. Start planning now. Test Functions in development stores and begin rolling out to production stores in early 2026. Waiting until the last month risks checkout disruptions for your merchants.

Example Migration: Line Item Script to Product Discount Function

Original Ruby Script (Script Editor):

Input.cart.line_items.each do |line_item|
if line_item.quantity >= 3
line_item.change_line_price(
line_item.line_price * 0.9,
message: "10% volume discount"
)
end
end

Output.cart = Input.cart

Migrated JavaScript Function:

export function run(input) {
const discounts = [];

for (const line of input.cart.lines) {
if (line.quantity >= 3) {
discounts.push({
value: { percentage: { value: "10" } },
targets: [{ productVariant: { id: line.merchandise.id } }],
message: "10% volume discount",
});
}
}

return {
discounts,
discountApplicationStrategy: "FIRST",
};
}

The Function achieves the same result but with an immutable input/output pattern instead of mutating the cart directly.

Execution Limits

Functions run in a constrained environment to protect checkout performance:

LimitValue
Execution time5ms
Memory11 MB
Wasm binary size256 KB
Input size64 KB
Output size64 KB
Network accessNone
File system accessNone
Staying Within Limits

Keep your Function logic simple and focused. Avoid deep nesting, large data structures, and complex string operations. If you hit the 5ms limit, profile your code and look for unnecessary iterations. Rust Functions are generally faster and use less memory than JavaScript.

Key Takeaways

  • Shopify Functions run custom logic inside Shopify's infrastructure as WebAssembly modules
  • They are fast (under 5ms), reliable (no external server), and scalable (runs on every checkout)
  • Extension targets cover discounts, shipping, payments, cart transforms, validation, and fulfillment
  • Write Functions in JavaScript (easier) or Rust (smaller, faster)
  • Input queries define what data your Function receives; outputs are declarative
  • Scripts are deprecated -- migrate to Functions before June 2026
  • Test locally with shopify app function run and unit tests

This concludes the Shopify Fundamentals module. You now have a deep understanding of the platform's architecture, APIs, templating, headless options, and extension runtime. In the next module, we explore how Claude Code transforms your Shopify development workflow.