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.

By RiteshMay 13, 202619 min read
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 discountCustomerRunFunction reads the authenticated customer's tier metafield and emits a percentage discount on every line item.

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.

b2b-tier-pricing/shopify.extension.tomltoml
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"

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.

src/run.graphqlgraphql
query Input {
  cart {
    buyerIdentity {
      customer {
        metafield(namespace: "b2b", key: "tier") { value }
      }
    }
    lines {
      id
      quantity
      cost {
        amountPerQuantity { amount }
      }
    }
  }
}

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.

src/lib.rs, the Function logicrust
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 deployfrom 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.rsrust
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 an across-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.rsrust
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.graphqlgraphql
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.

Adjacent reading from our Shopify and architecture clusters:

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.

The engagements this writing comes from

UK · Europe · Worldwide

selected work

Where these numbers came from

Production systems with the delivery figures attached.

Creoate product interface01
B2B commerce

The engineering partnership behind a cross-border wholesale marketplace.

Next.js storefront, Python ingestion pipelines, DynamoDB data layer and AWS infrastructure.

8+ yearsone team, still shipping
Ontick product interface02
Event technology

A commission-free ticketing platform built for ownership and scale.

Multi-organiser commerce, Stripe instalments and two native apps in one connected platform.

£2M+processed since launch
Easyship product interface03
Global logistics

Embedded product engineering for a global shipping platform.

Rate, tax and duty calculators, server-rendered courier pages and a custom MongoDB CMS.

550+couriers on the calculator
TEFL.ie product interface04
Education & training

A course-commerce and learning platform wired into one system.

WordPress and WooCommerce, a Moodle LMS, Stripe deposits and Zoho CRM, tied together with Zapier automation.

8 yrsengineer & run, since 2017
All White Laser product interface05
Medical aesthetics

A bespoke platform that finances aesthetic machines on Direct Debit.

A lead-to-billing system on GoCardless Direct Debit, provider certification, and a React Native app for machine owners.

9 yrsbuild & run, since 2017
Decofetch product interface06
Luxury commerce

A custom furniture marketplace engineered from storefront to infrastructure.

Server-rendered Next.js commerce over a Laravel API, bespoke operations tooling and re-architected AWS infrastructure.

0→livecustom, front to back
BA Engine Room product interface07
AI operations

An AI-native operating system that runs a consultancy lead to invoice.

Discovery briefs, e-signed contracts, Stripe deposits, delivery milestones and time tracking in one operational system.

0→1built from the ground up
PlusHeat product interface08
Home services

A conversion platform for a growing UK boiler-cover provider.

Custom plan configuration, postcode-qualified lead journeys, CRM synchronisation and campaign landing pages.

5 yrsweb partner since 2021
Léonia product interface09
Beauty commerce

A custom Shopify store for a French beauty brand.

Custom theme, customer accounts, loyalty rewards, referrals and gift-with-purchase offers.

5 yrspartners since 2021
Shutters 365 product interface10
Home improvement

Made-to-measure shutters with live pricing.

A seven-step product builder with live previews, sample orders and supplier tools.

7-stepconfigurator, live pricing
Bloc Ads Manager product interface11
Advertising

A self-service advertising platform for venues.

Campaign creation, audience targeting, in-app ads and reporting linked to venue check-ins.

check-insclosed-loop attribution
Bloc product interface12
Social events

An events app with the tools to run it.

Mobile app, backend, advertising tools, a digital marketplace and website.

4+ yrsone team, five codebases
Zonely product interface13
Social mobile

Two mobile apps for real-time companionship.

Customer and buddy apps with per-minute billing, wallets, moderation and admin tools.

2 appsconsumer + buddy, iOS & Android
Player Profile Hub product interface14
Grassroots football

Player profiles and discovery for youth football.

Verified profiles, video highlights, coach discovery and safeguarding on web and mobile.

0→1built from the ground up
DeepSpatial product interface15
Geospatial AI

Websites and a talent platform for DeepSpatial.

Corporate and investor pages, the Xploor talent platform and ongoing releases on AWS Amplify.

2 yrsone team, ongoing
Yippee Malta product interface16
Travel

Tour bookings with a custom mobile-first checkout.

A multilingual website connected to the booking API, with deposits, coupons and affiliate tracking.

90+core web vitals, mobile & desktop
Professional Energy product interface17
Energy brokerage

One platform for tenders, contracts and accounts.

Supplier tenders, contract management, brokerage accounting and client records.

100+suppliers in one tender

Tell us what you are trying to build.

A thirty-minute call with the engineer who would run it.

Book a call