Pattern library

    Shopify Functions for custom pricing — three patterns we ship in production

    B2B tier pricing, volume discounts, and verified-member rates — each as a complete Function, with the metafield design, theme integration and the deploy command. Written from nine Shopify Plus stores we have shipped on the new Functions API.

    May 13, 202619 min readBy Ritesh
    Three Shopify Functions patterns for custom pricing

    Why Functions, why now

    Shopify Scripts were the previous mechanism for custom checkout logic on Shopify Plus. They ran Ruby, they were limited to Plus, and Shopify announced their sunset for August 2025. Functions are the replacement: WebAssembly modules (Rust or JS source), running inside Shopify's infrastructure at every checkout decision point, with strict execution budgets (5ms / 256MB / 11MB output). They are not Plus-only for every use case — pricing-related Functions still require Plus, but shipping-rate and payment-customisation Functions work on all plans.

    We use Functions for three pricing patterns far more often than anything else. The rest of this post is the working code for each.

    Pattern 1 — B2B tier pricing

    Business case. A wholesale customer with a Bronze / Silver / Gold tier gets 5% / 10% / 15% off every line item, automatically, with no discount code entered.

    Mechanism. A discountCustomerRun Function reads the authenticated customer's tier metafield and emits a percentage discount on every line item.

    b2b-tier-pricing/shopify.extension.toml
    toml
    Function manifest. The targeting block declares which API the Function runs on; the input.graphql below dictates what data Shopify hands you at each invocation.
    api_version = "2024-10"
    name = "b2b-tier-pricing"
    type = "function"
    
    [build]
    command = "cargo build --release --target wasm32-wasip1"
    path = "target/wasm32-wasip1/release/b2b_tier_pricing.wasm"
    
    [ui.paths]
    create = "/"
    details = "/"
    
    [[extensions.targeting]]
    target = "purchase.product-discount.run"
    input_query = "src/run.graphql"
    export = "run"
    src/run.graphql
    graphql
    Tell Shopify what you actually need from the cart input. Asking for less keeps execution time down — we routinely run at 0.8-1.2ms with this query.
    query Input {
      cart {
        buyerIdentity {
          customer {
            metafield(namespace: "b2b", key: "tier") { value }
          }
        }
        lines {
          id
          quantity
          cost {
            amountPerQuantity { amount }
          }
        }
      }
    }
    src/lib.rs — the Function logic
    rust
    Pure Rust. Compiles to wasm32-wasip1. Shopify guarantees a 5ms budget per invocation; this function lands well under 2ms even on a 50-line cart.
    use shopify_function::prelude::*;
    use shopify_function::Result;
    
    generate_types!(
        query_path = "src/run.graphql",
        schema_path = "schema.graphql"
    );
    
    #[shopify_function]
    fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
        let tier = input
            .cart
            .buyer_identity
            .as_ref()
            .and_then(|b| b.customer.as_ref())
            .and_then(|c| c.metafield.as_ref())
            .map(|m| m.value.as_str())
            .unwrap_or("");
    
        let percent: f64 = match tier {
            "bronze" => 5.0,
            "silver" => 10.0,
            "gold" => 15.0,
            _ => return Ok(output::FunctionRunResult { discounts: vec![], discount_application_strategy: output::DiscountApplicationStrategy::First }),
        };
    
        let discounts = input
            .cart
            .lines
            .iter()
            .map(|line| output::Discount {
                value: output::Value::Percentage(output::Percentage { value: percent }),
                targets: vec![output::Target::ProductVariant(output::ProductVariantTarget {
                    id: line.id.clone(),
                    quantity: None,
                })],
                message: Some(format!("{}% B2B tier discount", percent as i32)),
            })
            .collect();
    
        Ok(output::FunctionRunResult {
            discounts,
            discount_application_strategy: output::DiscountApplicationStrategy::First,
        })
    }

    The metafield design that makes this work:

    Customer metafield setup (Settings → Custom data → Customer)
    text
    Namespace + key:   b2b.tier
    Type:              Single-line text
    Access:            Storefronts: read; Admin: read/write
    Values:            bronze | silver | gold | <unset>
    
    Recommended: limit valid values via a Shopify Flow workflow that
    fires on the "customer tag added" trigger and writes the metafield
    from a curated tag list.

    Deploy. shopify app deploy from the extension folder pushes the wasm binary and registers it. The Function then needs to be turned on as a Discount in the admin (Discounts > Create discount > Automatic discount > your Function). The activation is the part many teams miss the first time — the Function exists but won't run until it's wrapped in an active discount. When we ship Functions through our Shopify development engagement, the activation step is part of the merchant-facing handover doc, not a developer-only checklist.

    Pattern 2 — Volume discounts

    Business case. Buy 5 of any variant, get 10% off; buy 10, get 15% off; buy 25+, get 20% off. Applied per-variant, not across the cart total.

    Mechanism. Same Function target as Pattern 1, but the input query reads quantities and the logic applies a tiered percentage per line.

    volume-discount/src/lib.rs
    rust
    use shopify_function::prelude::*;
    use shopify_function::Result;
    
    generate_types!(
        query_path = "src/run.graphql",
        schema_path = "schema.graphql"
    );
    
    const TIERS: &[(i32, f64)] = &[
        (25, 20.0),
        (10, 15.0),
        (5,  10.0),
    ];
    
    #[shopify_function]
    fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
        let discounts = input
            .cart
            .lines
            .iter()
            .filter_map(|line| {
                let qty: i32 = line.quantity.into();
                let tier = TIERS.iter().find(|(threshold, _)| qty >= *threshold)?;
                Some(output::Discount {
                    value: output::Value::Percentage(output::Percentage { value: tier.1 }),
                    targets: vec![output::Target::ProductVariant(output::ProductVariantTarget {
                        id: line.id.clone(),
                        quantity: None,
                    })],
                    message: Some(format!("Volume discount: {}% off at qty {}+", tier.1 as i32, tier.0)),
                })
            })
            .collect();
    
        Ok(output::FunctionRunResult {
            discounts,
            discount_application_strategy: output::DiscountApplicationStrategy::First,
        })
    }

    The detail that catches teams: the threshold is the line-item quantity, not the cart total. If the customer adds 5 of variant A and 3 of variant B, only variant A gets the discount. If you want the discount to apply per-cart-total, the loop above sums first and applies a single value — two extra lines of Rust.

    The matching theme-side message that explains the math on the PDP, so customers know what they will get before they hit the cart:

    sections/product-volume-pricing.liquid (theme block)
    liquid
    <dl class="volume-pricing">
      {% assign tiers = '5,10|10,15|25,20' | split: '|' %}
      {% for t in tiers %}
        {% assign parts = t | split: ',' %}
        <dt>Buy {{ parts[0] }}+</dt>
        <dd>{{ parts[1] }}% off this variant</dd>
      {% endfor %}
    </dl>
    
    <p class="volume-pricing-note">
      Discount applies automatically at checkout. Per-variant.
    </p>

    Pattern 3 — Verified member rates

    Business case. Wholesale members get anacross-the-board rate (cost-plus, e.g. 30% off retail) on a subset of the catalogue tagged wholesale-eligible. The eligibility is verified via an external API call out (think VAT-number lookup, or membership status) — but Functions cannot make network calls.

    Mechanism. Solve the network-call limitation by lifting the verification result into a customer metafield, written by a background job that can call out. The Function then trusts the metafield.

    member-rate/src/lib.rs
    rust
    use shopify_function::prelude::*;
    use shopify_function::Result;
    
    generate_types!(
        query_path = "src/run.graphql",
        schema_path = "schema.graphql"
    );
    
    const MEMBER_DISCOUNT_PERCENT: f64 = 30.0;
    
    #[shopify_function]
    fn run(input: input::ResponseData) -> Result<output::FunctionRunResult> {
        let verified = input
            .cart
            .buyer_identity
            .as_ref()
            .and_then(|b| b.customer.as_ref())
            .and_then(|c| c.verified_member.as_ref())
            .map(|m| m.value == "true")
            .unwrap_or(false);
    
        if !verified {
            return Ok(output::FunctionRunResult {
                discounts: vec![],
                discount_application_strategy: output::DiscountApplicationStrategy::First,
            });
        }
    
        let discounts = input
            .cart
            .lines
            .iter()
            .filter(|line| {
                // only discount lines whose product has the eligibility tag
                line.merchandise
                    .as_ref()
                    .and_then(|m| m.product.as_ref())
                    .map(|p| p.has_wholesale_tag)
                    .unwrap_or(false)
            })
            .map(|line| output::Discount {
                value: output::Value::Percentage(output::Percentage { value: MEMBER_DISCOUNT_PERCENT }),
                targets: vec![output::Target::ProductVariant(output::ProductVariantTarget {
                    id: line.id.clone(),
                    quantity: None,
                })],
                message: Some("Wholesale member rate".to_string()),
            })
            .collect();
    
        Ok(output::FunctionRunResult {
            discounts,
            discount_application_strategy: output::DiscountApplicationStrategy::First,
        })
    }

    The input GraphQL pulls the verification flag and the product tag presence:

    src/run.graphql
    graphql
    query Input {
      cart {
        buyerIdentity {
          customer {
            verifiedMember: metafield(namespace: "b2b", key: "verified_member") { value }
          }
        }
        lines {
          id
          quantity
          merchandise {
            ... on ProductVariant {
              product {
                hasWholesaleTag: hasAnyTag(tags: ["wholesale-eligible"])
              }
            }
          }
        }
      }
    }

    The background job that owns the verification can be anywhere — we usually put it on the same Node API that handles the merchant's account portal. It listens for a customer-tag-added event (via a Shopify webhook), calls out to whatever verification service the merchant uses, and writes b2b.verified_member on the customer record via the Admin API. The Function then sees the result instantly on the next cart load. We build the webhook + admin-API layer behind this pattern as part of our API & integration engagement — usually 1-2 weeks alongside the Function itself.

    Performance and execution limits

    LimitValueWhat to do if you bump into it
    Execution time5 msNarrow the input query first; filter before you map; avoid string formatting in hot loops.
    Memory256 MBPractically unhittable for pricing; relevant for shipping Functions that pre-compute matrices.
    Output size11 MBA discount per cart line will not get close. Bulk shipping rates can.
    Network accessNoneUse the metafield-via-background-job pattern from Pattern 3.
    Active discount Functions per shop25Almost never relevant; bigger constraint is the 5 simultaneously-running discounts at checkout.

    Migrating from Shopify Scripts

    If you are coming from Scripts, three concepts move:

    • Input.cart maps to Input.cart in GraphQL — the structure is similar but you opt into fields rather than getting the whole cart. Tighter inputs run faster.
    • Customer-tag checks become metafield checks. Tags still work but they require a longer query. Metafields are quicker and more structured.
    • The cart.discount_code short-circuit goes away. Functions run alongside discount codes; if you want a Function to skip when a discount code is present, check for it in the input and early-return.

    The wider Shopify cost picture — whether you should be on Plus to use these patterns at all — is the subject of our companion Plus vs Advanced cost study. Most B2B merchants we run this work for are already on Plus for B2B catalogues; the few on Advanced need at minimum the Functions pricing API, which means an upgrade.

    ■ Related services

    Ship these Functions on your store

    The Shopify build engagement that includes Functions, the API engagement where the background metafield writer lives, and the retainer that keeps it all running:

    Frequently asked questions

    What's the difference between Shopify Functions and Scripts?
    Functions are WebAssembly modules (Rust or JS source) that run inside Shopify's infrastructure with strict 5ms / 256MB / 11MB execution budgets. Scripts were the Ruby-based predecessor, Plus-only, and are being sunset in August 2025. Functions are the modern replacement and are not Plus-only for every use case — pricing Functions still require Plus.
    Can a Shopify Function make a network call?
    No. Functions run inside a sandboxed runtime with no network access. The pattern for external verification (membership, VAT lookup, etc.) is to do the network work in a background job that writes the result to a customer metafield; the Function then reads the metafield at checkout.
    How fast does a Shopify pricing Function run in practice?
    Well under the 5ms budget for typical carts. A tight input GraphQL query against a 50-line cart lands at 0.8-1.2ms in our production deployments. The optimisation lever is the input query — ask for less data and execution time drops accordingly.
    Ritesh — Founding Partner, Appycodes

    About the author

    RiteshFounding Partner, Appycodes

    LinkedIn

    Ritesh leads engineering at Appycodes and has shipped Functions-based pricing on nine Plus stores in the last eighteen months — including the OEM Parts Store (B2B tier pricing across 70,000 SKUs) and Tierfutter Pro (volume discounts on perishable inventory). The three patterns above are the ones that come up most often; the underlying Rust scaffolding gets copied between projects almost verbatim.

    Last reviewed: May 13, 2026

    Full stack web and mobile tech company

    Taking the first step is the hardest. We make everything after that simple.

    Let's talk today