Shopify Functions 2026: Discounts, Delivery, Payment Customization
Shopify Functions run customer-defined code inside Shopify's checkout pipeline as WebAssembly modules. They replaced Shopify Scripts (Ruby, Plus-only) and now power custom discount logic, delivery rate manipulation, payment method filtering, cart validation, and order routing on every Shopify plan. This article focuses on the three function types most apps ship: discounts, delivery customization, and payment customization. We'll cover the input/output schemas, real Rust and JavaScript implementations, deployment, and the pitfalls we've hit running Functions in production.
Key Takeaways
- Functions execute in a WebAssembly sandbox with a 5 ms wall-clock budget and 200 KB memory cap
- Inputs come from a GraphQL query you define; outputs are typed JSON the platform applies
- Rust gives the most headroom for complex logic; JavaScript (via
function-runner) is fine for simple rules- Functions deploy as part of an app extension — one app can hold multiple functions of different types
- Discount Functions can stack with automatic discounts but follow Shopify's combination rules
- Functions are cached aggressively — the same input produces the same output, so don't rely on
Date.now()for time-sensitive logic
What's different about Shopify Functions
Functions are not webhooks. They run synchronously inside Shopify's request path — discounts on every cart update, delivery rates at the rate-shopping step, payment customization at checkout render. The merchant doesn't host them, you do, and you push them as part of an app.
Hard limits per execution:
| Resource | Limit |
|---|---|
| Wall clock | 5 ms |
| Memory | 200 KB |
| Input size | 64 KB |
| Output size | 64 KB |
| Network access | None |
| Filesystem | None |
If your function exceeds the budget, Shopify cancels it and applies the default behavior (no discount, full payment list, etc.). It does not error out the checkout.
Function types
| Function | Trigger | Mutation analog |
|---|---|---|
| Product discount | Cart update | Discounts collection |
| Order discount | Cart update | Discounts collection |
| Shipping discount | Cart update | Discounts collection |
| Delivery customization | Rate shopping | Delivery customizations |
| Payment customization | Checkout render | Payment customizations |
| Cart validation | Cart update | Cart Transform |
| Cart Transform | Cart update | Bundles |
| Order routing | Fulfillment | Routing |
We'll cover the first three families.
Discount Functions: a tiered VIP discount
Goal: Customers tagged vip get 15% off any cart over $200; tagged vip-plus get 20%.
// src/lib.rs
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct Input {
cart: Cart,
}
#[derive(Deserialize)]
struct Cart {
cost: Cost,
buyer_identity: Option<BuyerIdentity>,
lines: Vec<Line>,
}
#[derive(Deserialize)]
struct Cost { subtotal_amount: Money }
#[derive(Deserialize)]
struct Money { amount: String, currency_code: String }
#[derive(Deserialize)]
struct BuyerIdentity { customer: Option<Customer> }
#[derive(Deserialize)]
struct Customer { has_any_tag: bool, tags: Vec<String> }
#[derive(Deserialize)]
struct Line { id: String }
#[no_mangle]
pub extern "C" fn run() {
let input: Input = serde_json::from_reader(std::io::stdin()).unwrap();
let subtotal: f64 = input.cart.cost.subtotal_amount.amount.parse().unwrap();
let tags = input.cart.buyer_identity
.and_then(|b| b.customer)
.map(|c| c.tags)
.unwrap_or_default();
let percentage = if tags.contains(&"vip-plus".to_string()) && subtotal >= 200.0 {
20.0
} else if tags.contains(&"vip".to_string()) && subtotal >= 200.0 {
15.0
} else {
0.0
};
let output = if percentage > 0.0 {
json!({
"discounts": [{
"targets": input.cart.lines.iter().map(|l| json!({"cartLine": {"id": l.id}})).collect::<Vec<_>>(),
"value": { "percentage": { "value": format!("{:.1}", percentage) } }
}],
"discountApplicationStrategy": "MAXIMUM"
})
} else {
json!({ "discounts": [] })
};
println!("{}", output);
}
The matching GraphQL input query (in src/run.graphql):
query Input {
cart {
cost { subtotalAmount { amount currencyCode } }
buyerIdentity { customer { tags } }
lines { id }
}
}
Deploy with shopify app deploy from the CLI. The merchant then turns on the function in the discounts admin.
Discount combination rules
When discountApplicationStrategy is MAXIMUM, only the largest discount applies per line. FIRST applies in the order returned. Shopify's combinations setting on the discount itself (configured by the merchant, not the function) determines whether your function-discount can stack with manual or automatic discounts.
Delivery customization: hide rates over a weight threshold
Goal: For carts over 50 kg, hide the standard ground rate (it's unsafe; force a freight quote).
// src/run.js (JavaScript flavor)
export function run(input) {
const totalWeight = input.cart.deliveryGroups
.flatMap(g => g.cartLines)
.reduce((sum, line) => sum + line.merchandise.weight.value * line.quantity, 0);
if (totalWeight < 50) return { operations: [] };
const operations = [];
for (const group of input.cart.deliveryGroups) {
for (const opt of group.deliveryOptions) {
if (/standard|ground/i.test(opt.title)) {
operations.push({ hide: { deliveryOptionHandle: opt.handle } });
}
}
}
return { operations };
}
Operations the platform supports for delivery customization:
hide— remove a raterename— change the title shown to the buyermove— re-order rates
There is no add operation — Functions cannot create rates that don't already exist as Shopify shipping zones or third-party carrier responses. To create rates dynamically, you still need a CarrierService API endpoint.
Payment customization: hide COD on B2B orders
Goal: B2B orders (cart attribute buyer_type=b2b) shouldn't show Cash on Delivery.
export function run(input) {
const isB2B = input.cart.attribute?.value === 'b2b';
if (!isB2B) return { operations: [] };
const codMethods = input.paymentMethods.filter(pm =>
/cash on delivery|cod/i.test(pm.name)
);
return {
operations: codMethods.map(pm => ({ hide: { paymentMethodId: pm.id } })),
};
}
Same operation set as delivery: hide, rename, move. Payment customization runs at checkout render, so changes are immediate when buyers update their cart.
Local testing with function-runner
shopify app function run --input fixtures/vip-cart.json
fixtures/vip-cart.json matches the shape of your GraphQL input query. Drop in a few cases (no customer, customer without tag, VIP under threshold, VIP over, VIP-plus) and assert the output. Shopify's CLI runs the WASM through a local sandbox and prints the output JSON.
Deployment and versioning
shopify app deploy --message "VIP discount tiers v2"
Each deploy creates a new app version. Merchants stay on their current version until they explicitly update — or, in the case of automatic updates (set on the version), Shopify rolls them forward. Roll back via shopify app versions list and selecting an older version.
Limits, costs, and gotchas
- No randomness, no clocks: The runner is deterministic.
Math.random()is allowed but seeded;Date.now()returns a fixed value within a request. For time-based logic, use cart attributes set by your app's webhooks. - No outbound calls: Functions cannot call your backend. If you need external data (e.g., loyalty points balance), push it via metafields or cart attributes ahead of time.
- Cart Transform vs Discounts: Don't try to do bundles via discounts — use Cart Transform Functions, which can merge or expand line items.
- Per-shop function limit: 50 active functions per shop. App Store apps shouldn't burn the budget.
Comparing to Shopify Scripts (Plus-only, deprecated)
| Aspect | Shopify Scripts | Shopify Functions |
|---|---|---|
| Plan | Plus only | All plans |
| Language | Ruby | Rust, JS, AssemblyScript, any WASM-compatible |
| Execution | Ruby VM | WebAssembly sandbox |
| Hosting | Shopify hosts the script | Shopify hosts the WASM |
| Limits | 1 sec, 8 MB | 5 ms, 200 KB |
| Versioning | Inline edit | App deploy |
| Discoverability | Plus admin | Discounts admin |
Plus merchants have until end of 2026 to migrate Scripts to Functions. After that, Scripts stop running.
Frequently Asked Questions
Can I write Functions in Python or Go?
Not directly. Python doesn't compile to small-enough WASM, and Go's WASM output exceeds the 200 KB cap. Practical options are Rust, JavaScript (via Shopify's javy runtime), AssemblyScript, and TinyGo for very simple cases.
How do I A/B test a Function?
Deploy two app versions and route a portion of your merchant base to each. There's no built-in A/B test for Functions inside a single shop. For per-customer A/B, use cart attributes set by your client-side script.
Why is my Function not running?
Check three things: (1) the merchant has activated the function in their admin, (2) the function targets the correct discount class (product vs order vs shipping), (3) your input query returns the fields you reference. The Functions log in the merchant admin shows runtime errors.
How do Functions interact with the Cart API?
Cart Functions read the cart and can apply discounts, hide payment/delivery options, or transform line items. They don't mutate the cart object directly — Shopify applies the operations declaratively. For external mutations, use the Storefront Cart API.
What happens at scale?
Functions scale with the platform. We've run discount functions on Plus shops doing 30,000 cart updates/minute during BFCM with no degradation. The 5 ms budget is plenty for sane logic.
ECOSIRE has shipped 25+ Shopify Functions across discounts, delivery customization, and Cart Transform for Plus and standard merchants. Our Shopify customization team handles end-to-end Function development, App Store submission, and ongoing tuning.
Written by
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Scale Your Shopify Store
Custom development, optimization, and migration services for high-growth eCommerce.
Related Articles
Shopify App Bridge 4 Tutorial: Build Embedded Apps in 2026
Build Shopify embedded admin apps with App Bridge 4: session tokens, token exchange, navigation, modals, resource pickers, and Polaris React 13 setup.
Shopify GraphQL Admin API 2026: Complete Developer Guide
Master the Shopify Admin GraphQL API: queries, mutations, OAuth, calculated query cost, rate limits, bulk operations, and production code examples.
Shopify Markets 2026: International Pricing, Tax, Currency Setup
Configure Shopify Markets for global expansion: per-country pricing, automatic tax, multi-currency, geolocation, domain strategy, and Markets Pro tradeoffs.