How to Migrate a BOGO Script to Shopify Functions (Step-by-Step)
Walk through migrating a real Buy-One-Get-One Ruby script to a Shopify discount Function — with complete code, testing steps, and deployment instructions.
The Original Ruby Script
Here's a typical BOGO script from Script Editor. It gives the second item free for every pair:
# Buy One Get One Free
Input.cart.line_items.each do |item|
next if item.quantity < 2
pairs = item.quantity / 2
discount = pairs * item.variant.price
item.change_line_price(
item.line_price - discount,
message: "Buy one get one free!"
)
end
Output.cart = Input.cartThis script loops through cart line items, calculates how many pairs exist, and discounts the price of every second item to $0.
Step 1: Analyze the Script
First, let's understand what this script does in Shopify Functions terms:
- Script Type: Line Item Script
- Pattern: Buy X Get Y (BOGO)
- Target Function API:
product_discounts - Complexity: Simple (single pattern, no conditionals)
- Auto-migratable: Yes ✅
💡 Tip: Paste this script into ScriptShift's analyzer to see this analysis automatically, plus get the generated code below in seconds.
Step 2: The Generated Shopify Function
Here's the equivalent Shopify Function. You need three files:
src/run.js
// @ts-check
/**
* Buy One Get One Free — Product Discount Function
* Migrated from Script Editor by ScriptShift
*
* @param {InputQuery} input
* @returns {FunctionRunResult}
*/
export function run(input) {
const discounts = [];
const BUY_QTY = 1;
const GET_QTY = 1;
const DISCOUNT_PCT = 100; // 100 = free
for (const line of input.cart.lines) {
const sets = Math.floor(line.quantity / (BUY_QTY + GET_QTY));
if (sets > 0) {
discounts.push({
targets: [{ cartLine: { id: line.id } }],
value: {
percentage: {
value: DISCOUNT_PCT.toString(),
},
},
message: `Buy ${BUY_QTY} Get ${GET_QTY} Free`,
});
}
}
return {
discounts,
discountApplicationStrategy: "FIRST",
};
}input.graphql
query Input {
cart {
lines {
id
quantity
cost {
amountPerQuantity {
amount
currencyCode
}
}
merchandise {
... on ProductVariant {
id
product {
id
title
}
}
}
}
}
discountNode {
metafield(namespace: "scriptshift", key: "config") {
value
}
}
}shopify.extension.toml
name = "bogo-discount" type = "function" api_version = "2025-01" [build] command = "" path = "src/run.js" [[targeting]] target = "purchase.product-discount.run" input_query = "input.graphql" [ui.paths] create = "/" details = "/"
Step 3: Key Differences to Understand
Ruby change_line_price → Function discount targets
In Scripts, you directly mutated the line price. In Functions, you return discount objects that Shopify applies. This is more declarative — you say "apply 100% off to this line" rather than calculating the new price yourself.
Ruby loops → JavaScript for...of
The logic is nearly identical. Ruby's .each do |item| becomes JavaScript's for (const line of input.cart.lines).
No Output.cart assignment
Functions don't modify the cart directly. Instead, you return a result object with discounts (or operations for delivery/payment). Shopify applies them.
GraphQL input query is new
Scripts got the full cart object. Functions use a GraphQL query to request exactly what data they need. This makes them faster — less data to serialize.
Step 4: Deploy to Your Store
- Set up a Shopify app (if you don't have one):
shopify app init
- Generate a Function extension:
shopify app generate extension --type product_discounts --name bogo-discount
- Replace the generated files with the three files above
- Test locally:
shopify app function run --input '{...}' - Deploy:
shopify app deploy
- Create a discount in Shopify admin that uses the new Function
- Test checkout — add 2+ items, verify the second is free
Step 5: Validate Against Original Script
Before switching off the old Script:
- ✅ Test with 1 item (no discount should apply)
- ✅ Test with 2 items (second should be free)
- ✅ Test with 3 items (one free, one full price)
- ✅ Test with 10 items (5 free)
- ✅ Test with different products (discount applies per line)
- ✅ Test with mixed-price items (higher-priced item discounted correctly)
- ✅ Check that discount messages appear in checkout
Use the Migration Risk Simulator to run your new logic against 50+ simulated orders before going live.
Making It Configurable
Want to change the BOGO ratio without redeploying? Use metafields:
export function run(input) {
const discounts = [];
// Read config from metafield
const configStr = input.discountNode?.metafield?.value;
const config = configStr
? JSON.parse(configStr)
: { buyQty: 1, getQty: 1, discountPct: 100 };
for (const line of input.cart.lines) {
const sets = Math.floor(
line.quantity / (config.buyQty + config.getQty)
);
if (sets > 0) {
discounts.push({
targets: [{ cartLine: { id: line.id } }],
value: {
percentage: { value: config.discountPct.toString() },
},
message: `Buy ${config.buyQty} Get ${config.getQty} ${
config.discountPct === 100 ? 'Free' : config.discountPct + '% off'
}`,
});
}
}
return {
discounts,
discountApplicationStrategy: "FIRST",
};
}Set the metafield value to {"buyQty": 2, "getQty": 1, "discountPct": 50} for "Buy 2 Get 1 Half Off" — no code change needed.
Skip the manual work
ScriptShift generates this code automatically from any Ruby script. Paste, analyze, copy, deploy.
Try ScriptShift Free →