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
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 Target | What It Customizes | Example Use Case |
|---|---|---|
purchase.product-discount.run | Product-level discounts | "Buy 2, get 10% off" |
purchase.order-discount.run | Order-level discounts | "20% off orders over $100" |
purchase.shipping-discount.run | Shipping discounts | "Free shipping on orders over $50" |
purchase.payment-customization.run | Payment method visibility | Hide COD for international orders |
purchase.delivery-customization.run | Shipping option customization | Rename, reorder, or hide shipping rates |
cart-transform.run | Cart modifications | Auto-bundle products, merge line items |
purchase.validation.run | Cart/checkout validation | Enforce quantity limits, address rules |
fulfillment-constraints.run | Fulfillment routing | Route orders to specific locations |
order-routing.run | Order routing logic | Send 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
}
}
}
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,
})
}
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
| Date | Milestone |
|---|---|
| 2024 | Functions GA, Scripts deprecation announced |
| 2025 | No new Scripts can be created |
| January 2026 | Migration tools available in admin |
| June 2026 | Scripts permanently removed |
What Changes
| Shopify Scripts | Shopify Functions |
|---|---|
| Ruby language | JavaScript or Rust (compiled to Wasm) |
| Plus-only | Available on all plans |
| Script Editor app | Built into your app or standalone |
| Limited to line item, shipping, payment | Many more extension targets |
| No input query | Explicit GraphQL input query |
| Mutable cart object | Immutable input, declarative output |
Migration Strategy
- Audit existing Scripts: List all Scripts in use, their type (line item, shipping, or payment), and their logic.
- Map to Function targets: Line item scripts map to
product-discountororder-discount. Shipping scripts map todelivery-customization. Payment scripts map topayment-customization. - 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.
- Build a configuration UI: Scripts used the Script Editor; Functions need a merchant-facing UI in your app.
- Test thoroughly: Use the Function testing tools and verify against the same scenarios your Scripts handled.
- Deploy and activate: Install the Function on the store and deactivate the Script.
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:
| Limit | Value |
|---|---|
| Execution time | 5ms |
| Memory | 11 MB |
| Wasm binary size | 256 KB |
| Input size | 64 KB |
| Output size | 64 KB |
| Network access | None |
| File system access | None |
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 runand 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.