How to Build a Shopify Discount Function from Scratch
Updated March 2026 · 20 min read
With Shopify Scripts officially deprecated, every store using Ruby-based discount scripts needs to migrate to Shopify Functions. This tutorial walks you through building a product discount Function from zero — or you can use ScriptShift to auto-generate it from your existing script.
Prerequisites
- Node.js 18+ and npm
- Shopify CLI (
npm install -g @shopify/cli) - A Shopify Partner account and dev store
- Basic TypeScript knowledge
Step 1: Create the Function Extension
# In your Shopify app directory shopify app generate extension --type product_discounts # Choose TypeScript when prompted # This creates: # extensions/product-discount/ # ├── src/ # │ └── run.ts # Your discount logic # ├── input.graphql # What data you need from Shopify # ├── shopify.extension.toml # └── package.json
Step 2: Define Your Input Query
The input.graphql file tells Shopify what data to send to your Function. For a tiered discount based on quantity:
query RunInput {
cart {
lines {
quantity
merchandise {
... on ProductVariant {
id
product {
id
title
hasAnyTag(tags: ["bulk-eligible"])
}
}
}
cost {
amountPerQuantity {
amount
currencyCode
}
}
}
}
discountNode {
metafield(namespace: "discount-config", key: "tiers") {
value
}
}
}Step 3: Write the Discount Logic
Here's a complete tiered bulk discount Function in TypeScript. This replaces the classic Ruby tiered discount script:
// src/run.ts
import type {
RunInput,
FunctionRunResult,
Target,
Discount,
} from "../generated/api";
interface Tier {
threshold: number;
percentage: number;
message: string;
}
const DEFAULT_TIERS: Tier[] = [
{ threshold: 10, percentage: 20, message: "20% bulk discount" },
{ threshold: 5, percentage: 10, message: "10% bulk discount" },
{ threshold: 3, percentage: 5, message: "5% bulk discount" },
];
export function run(input: RunInput): FunctionRunResult {
// Read tiers from metafield config, or use defaults
let tiers = DEFAULT_TIERS;
const metafieldValue = input.discountNode.metafield?.value;
if (metafieldValue) {
try {
tiers = JSON.parse(metafieldValue);
} catch {
// Use defaults if metafield is invalid
}
}
// Sort tiers by threshold descending (highest first)
tiers.sort((a, b) => b.threshold - a.threshold);
const discounts: { targets: Target[]; value: Discount; message: string }[] = [];
for (const line of input.cart.lines) {
const variant = line.merchandise;
if (variant.__typename !== "ProductVariant") continue;
// Find applicable tier
const tier = tiers.find((t) => line.quantity >= t.threshold);
if (!tier) continue;
discounts.push({
targets: [
{
productVariant: { id: variant.id },
},
],
value: {
percentage: { value: tier.percentage.toString() },
},
message: tier.message,
});
}
if (discounts.length === 0) {
return { discounts: [], discountApplicationStrategy: "FIRST" };
}
return {
discounts,
discountApplicationStrategy: "FIRST",
};
}Step 4: Configure the Extension
# shopify.extension.toml api_version = "2025-04" [[extensions]] name = "Tiered Bulk Discount" handle = "tiered-bulk-discount" type = "product_discounts" [extensions.build] command = "npm run build" path = "dist/function.wasm" [extensions.ui] enable = true handle = "tiered-bulk-discount-ui"
Step 5: Test and Deploy
# Build and test locally npm run build shopify app function run --export run # Deploy to your dev store shopify app deploy # Create the discount in Shopify Admin: # Settings → Discounts → Create → Automatic discount # Choose your "Tiered Bulk Discount" function
Common Gotchas
- WASM size limit: Functions compile to WebAssembly with a 256KB limit. Keep dependencies minimal — no large libraries.
- No network calls: Functions run in a sandbox. You can't make HTTP requests. Use metafields for configuration data.
- Execution time: Functions must complete in 5ms. Keep logic simple and avoid nested loops over large datasets.
- Testing: Use
shopify app function runwith JSON input files. Create test cases for edge cases (empty cart, single item, max quantity).
The Easier Way
If you have an existing Ruby script and just want working Function code, ScriptShift can analyze your script and generate the complete Function project — including the input query, TypeScript logic, TOML config, and test files. Free analysis, instant results.
Migrate automatically
Paste your Ruby script → get working Shopify Functions code in seconds.
Try ScriptShift Free →