Shopify Functions Reference
Shopify Functions replace Scripts as the way to customise backend logic. They run in a WebAssembly sandbox with strict performance guarantees.
Overview
| Item | Value |
|---|---|
| Runtime | WebAssembly (Wasm) |
| Languages | Rust (recommended), JavaScript (via Javy) |
| Execution limit | 11ms instruction count limit |
| Memory limit | 64 KB linear memory |
| Input/Output | JSON (stdin/stdout) |
| API version | 2026-01 |
Function Types
| Type | Purpose | Replaces |
|---|---|---|
product_discounts |
Automatic product discounts | Shopify Scripts (line item) |
order_discounts |
Order-level discounts | Shopify Scripts (order) |
shipping_discounts |
Shipping rate discounts | Shopify Scripts (shipping) |
payment_customization |
Hide/reorder payment methods | Shopify Scripts |
delivery_customization |
Hide/reorder/rename delivery options | Shopify Scripts |
cart_transform |
Merge/expand/update cart lines | New |
fulfillment_constraints |
Constrain fulfillment locations | New |
order_routing_location_rule |
Custom order routing | New |
cart_checkout_validation |
Validate cart at checkout | New |
Getting Started
Create a Function
# Generate a new function extension
shopify app generate extension --template discount_function_rust
# or
shopify app generate extension --template discount_function_javascript
# Structure created:
extensions/my-discount/
├── src/
│ ├── main.rs # Rust: function logic
│ └── run.graphql # Input query
├── Cargo.toml # Rust dependencies
├── shopify.extension.toml
└── schema.graphql # Generated API schema
Input Query (run.graphql)
Define what data your function receives:
query RunInput {
cart {
lines {
quantity
merchandise {
... on ProductVariant {
id
product {
id
hasAnyTag(tags: ["VIP"])
}
}
}
cost {
amountPerQuantity {
amount
currencyCode
}
}
}
}
discountNode {
metafield(namespace: "discount", key: "config") {
value
}
}
}
Rust Implementation
use shopify_function::prelude::*;
use shopify_function::Result;
#[shopify_function_target(rename = "function")]
fn function(input: input::ResponseData) -> Result<output::FunctionRunResult> {
let config: serde_json::Value = serde_json::from_str(
input.discount_node.metafield
.as_ref()
.map(|m| m.value.as_str())
.unwrap_or("{}"),
)?;
let percentage = config.get("percentage")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
let targets: Vec<output::Target> = input
.cart
.lines
.iter()
.filter_map(|line| {
let variant = match &line.merchandise {
input::InputCartLinesMerchandise::ProductVariant(v) => v,
_ => return None,
};
if variant.product.has_any_tag {
Some(output::Target::ProductVariant(
output::ProductVariantTarget {
id: variant.id.clone(),
quantity: None,
},
))
} else {
None
}
})
.collect();
if targets.is_empty() {
return Ok(output::FunctionRunResult {
discounts: vec![],
discount_application_strategy:
output::DiscountApplicationStrategy::FIRST,
});
}
Ok(output::FunctionRunResult {
discounts: vec![output::Discount {
message: Some(format!("{}% VIP discount", percentage)),
targets,
value: output::Value::Percentage(output::Percentage {
value: percentage.to_string(),
}),
}],
discount_application_strategy:
output::DiscountApplicationStrategy::FIRST,
})
}
JavaScript Implementation
// src/run.js
export function run(input) {
const config = JSON.parse(
input?.discountNode?.metafield?.value ?? "{}"
);
const percentage = parseFloat(config.percentage) || 0;
const targets = input.cart.lines
.filter((line) => {
return line.merchandise?.__typename === "ProductVariant"
&& line.merchandise.product.hasAnyTag;
})
.map((line) => ({
productVariant: { id: line.merchandise.id },
}));
if (targets.length === 0) {
return { discounts: [], discountApplicationStrategy: "FIRST" };
}
return {
discounts: [
{
message: `${percentage}% VIP discount`,
targets,
value: {
percentage: { value: percentage.toString() },
},
},
],
discountApplicationStrategy: "FIRST",
};
}
Configuration
shopify.extension.toml
api_version = "2026-01"
[[extensions]]
name = "VIP Discount"
handle = "vip-discount"
type = "function"
[extensions.build]
command = "cargo wasi build --release"
path = "target/wasm32-wasi/release/vip-discount.wasm"
# For JavaScript:
# command = "npx javy compile src/run.js -o dist/function.wasm"
# path = "dist/function.wasm"
[extensions.targeting]
target = "purchase.product-discount.run"
[extensions.ui]
handle = "vip-discount-ui"
[extensions.input.variables]
namespace = "discount"
key = "config"
Testing
Unit tests (Rust)
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_discount_without_tag() {
let input = input::ResponseData {
cart: input::InputCart {
lines: vec![/* line without VIP tag */],
},
discount_node: input::InputDiscountNode {
metafield: Some(input::InputDiscountNodeMetafield {
value: r#"{"percentage": 10}"#.to_string(),
}),
},
};
let result = function(input).unwrap();
assert!(result.discounts.is_empty());
}
}
Local testing
# Test with sample input
shopify app function run --path extensions/my-discount
# Build and preview
shopify app dev
Deployment
# Deploy function with app
shopify app deploy
# Functions are versioned with the app
# Each deploy creates a new function version
Migration from Scripts
| Scripts | Functions |
|---|---|
| Ruby-like DSL | Rust or JavaScript (Wasm) |
| Online Store only | All channels (POS, B2B, headless) |
| Limited to 3 types | 10+ function types |
| No version control | Git-based, CI/CD ready |
| Script Editor app | Shopify CLI |
| Shopify-hosted | Developer-hosted logic |
Migration steps:
- Identify Scripts in use (Settings > Apps > Script Editor)
- Map each Script to a Function type (see table above)
- Rewrite logic in Rust or JavaScript
- Test with
shopify app function run - Deploy and activate via Shopify admin
- Disable old Scripts
Performance Guidelines
- Functions must complete within the instruction count limit (~11ms equivalent)
- Minimise allocations - reuse buffers where possible
- Avoid complex string operations in hot paths
- Rust compiles to smaller, faster Wasm than JavaScript
- Use
cargo wasi build --releasefor optimised builds - Profile with
shopify app function run --export-timing
Best Practices
- Use Rust for production - smaller binaries, faster execution
- Use JavaScript for prototyping - faster iteration, familiar syntax
- Keep input queries minimal - request only the fields you need
- Store configuration in metafields - avoid hardcoded values
- Test edge cases - empty carts, missing metafields, zero quantities
- Version your functions - use semantic versioning with app deploys
- Monitor execution - check function logs in Partner Dashboard