TutorialFebruary 28, 2026· 8 min read

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.cart

This 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

  1. Set up a Shopify app (if you don't have one):
    shopify app init
  2. Generate a Function extension:
    shopify app generate extension --type product_discounts --name bogo-discount
  3. Replace the generated files with the three files above
  4. Test locally:
    shopify app function run --input '{...}'
  5. Deploy:
    shopify app deploy
  6. Create a discount in Shopify admin that uses the new Function
  7. 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 →