Why Every Shopify Developer Needs to Learn Functions Right Now
Shopify Scripts officially stops executing on June 30, 2026. If you're still relying on Scripts for custom discount logic, the clock is already ticking — no new Scripts or edits are allowed after April 15, 2026, according to Shopify's deprecation timeline.
The replacement? Shopify Functions — server-side WebAssembly modules that run directly on Shopify's global infrastructure in under 5 milliseconds. They're faster, more powerful, and available across every purchase surface from checkout to POS.
This Shopify Functions tutorial walks you through building your first custom discount from scratch. You'll set up your development environment, write the function logic, configure the merchant-facing UI, test locally, and deploy to a live store. Whether you pick Rust or JavaScript, you'll have a working discount function by the end. If you're new to Shopify app development, this guide assumes basic familiarity with the Shopify CLI and Partner Dashboard.
What Are Shopify Functions and How Do They Work?
Shopify Functions are small, sandboxed programs that run inside Shopify's infrastructure during critical purchase flows. Unlike app proxies or webhooks that add network latency, Functions execute inline — right alongside Shopify's own checkout logic.
Every Function follows the same three-step model:
- Input — Shopify sends your function a JSON payload defined by a GraphQL query you write. You choose exactly which cart, product, and customer data you need.
- Logic — Your code processes that input and makes decisions. This runs as a compiled WebAssembly (Wasm) module.
- Output — Your function returns a structured JSON response that tells Shopify what to do (apply a discount, reject a payment method, etc.).
Where Functions Run
Functions aren't hosted on your server. Shopify compiles your code to WebAssembly, uploads it to their edge infrastructure, and executes it within the checkout pipeline. This means zero cold starts and consistent sub-5ms execution times, according to Shopify's Functions documentation.
What Functions Can Do
Discounts are the most common use case, but Functions power seven different extension points:
- Discounts — product, order, and shipping discounts
- Payment customizations — hide, rename, or reorder payment methods
- Delivery customizations — modify shipping options at checkout
- Cart validation — enforce rules before checkout completion
- Order routing — control fulfillment location logic
- Bundles — create custom product bundles
- Fulfillment constraints — set delivery promises and constraints
For this tutorial, we're focused on the discount API — the most practical starting point for most developers.
Prerequisites: Setting Up Your Environment

Before writing any code, you need three things configured on your machine.
Developer Accounts and Tools
| Requirement | Details |
|---|---|
| Shopify Partner account | Free at partners.shopify.com |
| Development store | Create one from your Partner Dashboard (free, no subscription needed) |
| Node.js 22+ | Required for the Shopify CLI |
| Shopify CLI | Install via npm install -g @shopify/cli |
| Rust toolchain | If using Rust: install via rustup.rs, then add the Wasm target |
If you're choosing Rust (recommended for production), add the WebAssembly compilation target:
rustup target add wasm32-unknown-unknownFor JavaScript, no additional toolchain is needed — the Shopify CLI handles compilation through Javy (a QuickJS-based Wasm compiler) automatically.
Choosing Between Rust and JavaScript
This is the first architectural decision you'll face, and it matters more than you might think.
| Factor | Rust | JavaScript |
|---|---|---|
| Performance | Fastest — compiles directly to Wasm | ~3x slower than Rust equivalent |
| Instruction budget | Uses far fewer of the 11M instruction limit | Can hit limits with complex logic |
| Learning curve | Steeper if you're new to Rust | Familiar syntax for web developers |
| Best for | Production apps, complex logic, public apps | Prototyping, simple discounts, custom apps |
| Binary size | Smaller compiled output | ~220 bytes + bytecode via dynamic linking |
One developer documented a 7x reduction in instruction count after migrating a discount function from JavaScript to Rust. For prototyping, JavaScript is perfectly fine. For anything headed to the Shopify App Store, strongly consider Rust.
Scaffolding Your Shopify Functions App
Let's build. Start by creating a new Shopify app if you don't already have one:
npm init @shopify/app@latestFollow the prompts to name your app and select your preferred framework. Once the app is created, navigate into the project directory and generate a discount function extension:
cd your-app-name
shopify app generate extension --template discount --name my-first-discountThe CLI scaffolds everything you need inside extensions/my-first-discount/. Here's what gets created:
extensions/my-first-discount/
├── shopify.extension.toml # Extension configuration
├── src/
│ ├── cart_lines_discounts_generate_run.graphql # Input query
│ ├── cart_lines_discounts_generate_run.rs # Function logic (Rust)
│ └── ...
├── Cargo.toml # Rust dependencies (if using Rust)
└── input.graphql # Alternative input query locationUnderstanding the Configuration File
Open shopify.extension.toml — this is where Shopify learns what your function does:
api_version = "2026-04"
[[extensions]]
name = "my-first-discount"
handle = "my-first-discount"
type = "function"
[[extensions.targeting]]
target = "cart.lines.discounts.generate.run"
input_query = "src/cart_lines_discounts_generate_run.graphql"
export = "run"The target field tells Shopify which extension point your function plugs into. For product and order discounts, use cart.lines.discounts.generate.run. For shipping discounts, you'd add a second targeting block with cart.delivery-options.discounts.generate.run.
Writing Your First Discount Function

Now for the core logic. We'll build a "spend $100, get 10% off" automatic discount — simple enough to understand the architecture, complex enough to be useful.
Step 1: Define Your Input Query
Open the GraphQL input query file and specify exactly what data your function needs from the cart:
query RunInput {
cart {
cost {
subtotalAmount {
amount
currencyCode
}
}
lines {
id
quantity
merchandise {
... on ProductVariant {
id
product {
title
}
}
}
cost {
totalAmount {
amount
}
}
}
}
discountNode {
metafield(namespace: "my-discount", key: "config") {
value
}
}
}Only query what you actually need. Every extra field costs deserialization instructions against your 11-million instruction budget. This is one of the most common performance mistakes new developers make.
Step 2: Write the Function Logic (Rust)
Here's the complete Rust implementation:
use shopify_function::prelude::*;
use shopify_function::run_function_with_input;
#[typeshare]
#[derive(Deserialize)]
pub struct Config {
pub minimum_amount: f64,
pub discount_percentage: f64,
}
#[shopify_function]
fn run(input: RunInput) -> FunctionRunResult {
let config: Config = input
.discount_node
.metafield
.as_ref()
.map(|m| serde_json::from_str(&m.value).unwrap())
.unwrap_or(Config {
minimum_amount: 100.0,
discount_percentage: 10.0,
});
let cart_total: f64 = input
.cart
.cost
.subtotal_amount
.amount
.parse()
.unwrap_or(0.0);
if cart_total < config.minimum_amount {
return Ok(FunctionRunResult {
discount_application_strategy: DiscountApplicationStrategy::FIRST,
discounts: vec![],
});
}
let targets: Vec<Target> = input
.cart
.lines
.iter()
.map(|line| Target::OrderSubtotal {
excluded_variant_ids: vec![],
})
.collect();
Ok(FunctionRunResult {
discount_application_strategy: DiscountApplicationStrategy::FIRST,
discounts: vec![Discount {
message: Some(format!(
"Spend ${:.0}+ and save {:.0}%!",
config.minimum_amount, config.discount_percentage
)),
targets,
value: Value::Percentage(Percentage {
value: config.discount_percentage.to_string(),
}),
conditions: None,
}],
})
}Step 3: Write the Function Logic (JavaScript Alternative)
If you're prototyping in JavaScript, here's the equivalent:
// @ts-check
/**
* @param {RunInput} input
* @returns {FunctionRunResult}
*/
export function run(input) {
const config = JSON.parse(
input?.discountNode?.metafield?.value ?? '{}'
);
const minimumAmount = config.minimumAmount ?? 100;
const discountPercentage = config.discountPercentage ?? 10;
const cartTotal = parseFloat(input?.cart?.cost?.subtotalAmount?.amount ?? '0');
if (cartTotal < minimumAmount) {
return { discountApplicationStrategy: 'FIRST', discounts: [] };
}
return {
discountApplicationStrategy: 'FIRST',
discounts: [
{
message: `Spend $${minimumAmount}+ and save ${discountPercentage}%!`,
targets: [{ orderSubtotal: { excludedVariantIds: [] } }],
value: {
percentage: { value: discountPercentage.toString() },
},
},
],
};
}Important JavaScript limitation: Javy compiles ES2020 syntax but provides no event loop. Do not use async/await, fetch, setTimeout, or any Node.js/browser APIs — they compile but throw runtime errors, as detailed in Shopify's JavaScript for Functions guide.
Building the Merchant Configuration UI

Your function logic is only half the equation. Merchants need a way to configure the discount — setting the minimum spend threshold and discount percentage — without touching code.
Shopify Functions communicate with their configuration UI through metafields. You build an Admin UI extension that saves settings to a metafield, and your function reads that metafield at runtime.
Creating the UI Extension
Generate an Admin UI extension alongside your function:
shopify app generate extension --template discount_ui --name my-discount-uiThis creates a React-based UI using Shopify's Remote UI components. The key components you'll use:
<BlockStack>— vertical layout<TextField>— number inputs for threshold and percentage<Text>— labels and descriptions<Banner>— validation messages
The UI extension reads and writes to the same metafield namespace (my-discount, key config) that your function queries in its GraphQL input. When a merchant saves the configuration, Shopify stores it as a JSON string in the metafield. When the function executes, it deserializes that string back into your config struct.
For the full UI extension implementation pattern, refer to Shopify's guide on building discount UI extensions.
Testing Your Shopify Functions Discount Locally
Testing Functions locally is critical. You don't want to deploy to a store just to discover your logic has a bug.
Using Function Replay
The Shopify CLI includes a replay tool that lets you execute your function against real cart data captured from your development store:
shopify app function replayThis command:
- Pulls recent function execution logs from your dev store
- Lets you select a specific execution
- Reruns your function locally with that exact input
- Shows you the output so you can verify correctness
Manual Testing with function-runner
For more control, use the function-runner tool directly:
npx function-runner -f 'dist/function.wasm' -e run -i 'test-input.json'Create a test-input.json file that mimics a real cart payload:
{
"cart": {
"cost": {
"subtotalAmount": { "amount": "150.00", "currencyCode": "USD" }
},
"lines": [
{
"id": "gid://shopify/CartLine/1",
"quantity": 2,
"merchandise": {
"__typename": "ProductVariant",
"id": "gid://shopify/ProductVariant/123",
"product": { "title": "Test Product" }
},
"cost": { "totalAmount": { "amount": "150.00" } }
}
]
},
"discountNode": {
"metafield": {
"value": "{\"minimumAmount\": 100, \"discountPercentage\": 10}"
}
}
}What to Test
| Scenario | Expected Result |
|---|---|
| Cart total below threshold ($50) | No discount applied, empty discounts array |
| Cart total at threshold ($100) | 10% discount applied |
| Cart total above threshold ($200) | 10% discount applied |
| Missing metafield config | Falls back to default values ($100/10%) |
| Empty cart | No discount, no errors |
Test edge cases aggressively. When a function exceeds its instruction limit or throws an error in production, it fails silently — the discount simply doesn't appear, with no error message shown to the customer.
Deploying Your Custom Discount to a Live Store
Once your function passes local testing, deployment is a two-step process.
Step 1: Deploy the App
shopify app deployThis compiles your function to WebAssembly, uploads it to Shopify's infrastructure, and registers the extension. The CLI handles versioning — each deployment creates a new version of your app.
Step 2: Activate the Discount
After deployment, create the actual discount in your store:
- Go to Shopify Admin > Discounts > Create discount
- Select your custom discount type (it appears under the app name)
- Configure the settings through your UI extension
- Set the discount to Active
Alternatively, create the discount programmatically via the GraphQL Admin API:
mutation {
discountAutomaticAppCreate(
automaticAppDiscount: {
title: "Spend $100, Get 10% Off"
functionId: "your-function-id"
startsAt: "2026-04-03T00:00:00Z"
metafields: [
{
namespace: "my-discount"
key: "config"
type: "json"
value: "{\"minimumAmount\":100,\"discountPercentage\":10}"
}
]
}
) {
automaticAppDiscount {
discountId
}
userErrors {
field
message
}
}
}For merchants on Shopify Plus, custom app functions unlock the most flexibility. Public app functions work on all Shopify plans.
Discount Application Strategies and Stacking

One of the most misunderstood aspects of Shopify Functions is how multiple discounts interact. Understanding this correctly prevents hours of debugging.
The discountApplicationStrategy Field
Your function's output includes a discountApplicationStrategy field with three options:
| Strategy | Behavior |
|---|---|
| FIRST | Applies the first qualifying discount and ignores the rest |
| MAXIMUM | Applies whichever single discount gives the customer the biggest savings |
| ALL | Applies every qualifying discount (they stack) |
Critical distinction: This strategy controls how discount candidates within a single function compete. It does not control how your function's discounts interact with other discount functions or manual discounts.
Cross-Function Stacking
Global discount stacking is controlled by the combinesWith property on the discount node — not the application strategy. A single checkout can run up to 25 discount functions concurrently, each operating in isolation with no knowledge of the others, as documented in the Shopify Discount Function API reference.
Confusing selection strategies with stacking rules is one of the most common mistakes developers make. Your function decides which of its own discount candidates to propose. Shopify's discount engine then decides which proposed discounts from all functions actually apply based on combination rules.
Common Mistakes When Building Shopify Functions
After working with Functions and studying developer experiences across the ecosystem, these are the pitfalls that catch most builders.
Performance Mistakes
- Querying unnecessary input fields — Every GraphQL field you request costs deserialization instructions. A developer profiled their function and found JSON serialization alone consumed 5.94 million of their 11 million instruction budget. Trim your input query ruthlessly.
- Starting with JavaScript for complex logic — JS works great for simple discounts. But if your function processes large catalogs or runs complex calculations, you'll hit the instruction limit fast. The Discount Kit team saw a 30% drop in instruction counts after optimizing their Wasm approach.
- Ignoring the 256KB module size limit — Especially relevant for Rust developers pulling in large crate dependencies. Keep your dependency tree lean.
Architectural Mistakes
- Confusing Cart Transform with Discount Functions — Cart Transform changes cart structure and presentation. Discount Functions change pricing. Mixing responsibilities creates brittle, hard-to-debug behavior.
- Not handling missing metafields — If a merchant deletes or corrupts the configuration metafield, your function should fall back gracefully, not crash.
- Skipping monitoring — Functions fail silently. Set up monitoring through Partner Dashboard > Apps > Insights > Functions to catch instruction limit overruns and errors before merchants report them.
Migration Mistakes
| Mistake | Consequence | Prevention |
|---|---|---|
| Delaying Scripts migration | Store breaks after June 30, 2026 | Start migrating now — no edits allowed after April 15, 2026 |
| 1:1 porting Script logic | Bloated, inefficient Functions | Redesign logic for the input/output model |
| Not testing with real cart data | Unexpected edge cases in production | Use shopify app function replay with real executions |
| Hardcoding configuration | Requires redeployment for every change | Use metafields for merchant-configurable settings |
Advanced Patterns: Beyond Basic Discounts
Once you're comfortable with the fundamentals, these patterns unlock more sophisticated discount logic.
Tiered Volume Discounts
Instead of a flat threshold, implement progressive tiers — 5% off at $50, 10% at $100, 15% at $200. Parse the tiers from your metafield configuration and iterate through them to find the highest qualifying tier.
Customer-Specific Discounts
Query cart.buyerIdentity in your input to access customer tags and metafields. This lets you build loyalty-tier discounts, wholesale pricing, or first-time buyer promotions — all running natively in the checkout without app proxy latency.
Product-Targeted Discounts
Instead of targeting orderSubtotal, target specific product variants using productVariant targets. This lets you run "Buy 2 of Product X, get 20% off" promotions that only discount the qualifying items.
For working code examples of all these patterns, explore the Shopify Function Examples repository on GitHub, which includes discount implementations in both Rust and JavaScript.
Combining Multiple Discount Classes
A single function can target product discounts, order discounts, and shipping discounts simultaneously by defining multiple targeting blocks in your shopify.extension.toml. This reduces the number of functions you need to maintain and keeps related logic together.
Performance Optimization and Monitoring

Shopify Functions run in a highly constrained environment. Optimizing performance isn't optional — it's a requirement for production reliability.
Instruction Budget Management
Your function gets 11 million instructions per execution. Here's how to stay well within budget:
- Profile locally — Run
npx function-runnerwith the--statsflag to see exact instruction counts - Minimize input query — Only request GraphQL fields your logic actually reads
- Avoid large allocations — Pre-allocate vectors in Rust; avoid building large intermediate arrays in JavaScript
- Use early returns — If the cart doesn't qualify, return an empty discounts array immediately without processing line items
Monitoring in Production
After deployment, monitor your function's health through the Partner Dashboard:
- Navigate to Apps > Your App > Insights > Functions
- Check execution success rate — anything below 99.9% needs investigation
- Review instruction count distribution — p50, p75, and p99 values
- Watch for instruction limit errors — these mean your function hit the 11M cap and failed silently
The Shopify engineering team's deep dive into JavaScript WebAssembly performance provides excellent benchmarks for setting performance expectations. JavaScript functions typically complete well within the 5ms window for realistic use cases, but watch instruction counts carefully as cart complexity grows.
Shopify Functions Tutorial: Your Next Steps
You've just walked through the complete lifecycle of building your first custom discount with Shopify Functions — from environment setup through deployment and monitoring. The input-logic-output model is straightforward once you've built your first function, and the patterns scale naturally to more complex discount scenarios.
Here's your action plan:
- Set up your environment today — Partner account, dev store, Shopify CLI, and your chosen language toolchain
- Build the spend-threshold discount from this tutorial as your learning project
- Test with real cart data using
shopify app function replaybefore deploying - Monitor instruction counts in the Partner Dashboard after going live
- Migrate any existing Scripts before the April 15, 2026 edit freeze
If you're building on Shopify and want to connect with other developers tackling Functions, app development, and checkout customization, join the Talk Shop community where Shopify developers share implementation patterns and debug production issues together.
What discount logic are you planning to build with Shopify Functions? Whether it's tiered pricing, customer-specific offers, or bundle discounts, the architecture you learned here applies to all of them. Start with the simplest version, get it deployed, and iterate from there.

About Talk Shop
The Talk Shop team — insights from our community of Shopify developers, merchants, and experts.
Related Insights
The ecommerce newsletter that's actually useful.
Daily trends, teardowns, and tactics from the top 1% of ecommerce brands. Delivered every morning.
