Skip to content
Shopify niche topics (gap-fill)

Shopify Functions for SMBs: Three Use-Cases Worth Building

Shopify Functions for SMBs: three use-cases worth building, discount, delivery and payment customisation, with starter code and honest build-cost notes.

By WitsCode9 min read
Shopify niche topics (gap-fill)

For most small Shopify stores, three Shopify Functions are worth the build cost, and the rest can wait. A discount Function, for when you need volume, bundle or tiered pricing logic that Shopify's native automatic discounts cannot express. A delivery customisation Function, for when you need to hide, rename or reorder shipping methods at checkout based on what is in the cart or where it is going. And a payment customisation Function, for when you need to hide a payment method under a condition, such as removing cash on delivery on orders above a value. Those three solve real, recurring problems on small stores, and they are small enough builds that the arithmetic works.

A Shopify Function is a piece of custom code that runs on Shopify's own servers at a specific point in the checkout and order flow. It replaced the old Shopify Scripts feature, which was locked to Shopify Plus, and the important change for smaller merchants is that Functions are now available on every plan. The capability that used to be enterprise-only is no longer. A Function is worth building, rather than renting an app for the same job, when the rule is stable, specific to your store, and an app would otherwise charge you a monthly fee indefinitely for logic you could encode once. This article walks through the three SMB use-cases that meet that test, with starter code for each, and it is honest about the cases where you should still just install an app.

What a Shopify Function actually is

Before the use-cases, a quick grounding, because the term gets used loosely. A Function is custom logic that Shopify runs server-side. You do not host it, you do not pay per execution, and it runs inside Shopify's checkout in the milliseconds before a customer sees their options. Functions are written in a language that compiles to WebAssembly, which in practice means Rust or JavaScript using Shopify's command-line tooling. Each Function declares an input query, a small GraphQL request for exactly the cart and checkout data it needs, and then returns a set of operations: apply this discount, hide that delivery option, rename this payment method.

One clarification worth making early. Functions change behaviour and logic. They do not change how the checkout looks. If you want a custom message or an extra field on the checkout page, that is a checkout UI extension, a different feature. Functions are the rules engine, not the paint. Keep that distinction and the three use-cases below will make sense.

Functions are deployed as part of a Shopify app. For a single store that simply means a custom app built for you, which is normal and fine. It does mean there is a small thing to maintain, since the Function APIs are versioned, but the upkeep is light and occasional rather than constant.

Use-case one: a discount Function that automatic discounts cannot match

Shopify's native automatic discounts are good at simple shapes. A flat percentage off a collection. A basic buy-one-get-one. Where they run out of road is conditional and tiered logic. Suppose you want true volume pricing, where six units of an item earns five percent and twelve units earns twelve percent. Or a bundle discount that only triggers when a specific set of SKUs is in the cart together. Or a returning-customer threshold that behaves differently from your code discounts. The native discount editor cannot express those cleanly, and that is exactly the gap a discount Function fills.

Here is the shape of a Rust product discount Function that applies a tiered percentage once the cart quantity crosses a threshold. It is trimmed to the logic that teaches the idea.

fn run(input: Input) -> Result<FunctionResult> {
    let total_qty: i64 = input.cart.lines.iter()
        .map(|line| line.quantity)
        .sum();

    let percentage = match total_qty {
        q if q >= 12 => 12.0,
        q if q >= 6  => 5.0,
        _ => return Ok(FunctionResult { discounts: vec![], ..Default::default() }),
    };

    Ok(FunctionResult {
        discounts: vec![Discount {
            value: Value::Percentage(Percentage { value: percentage }),
            targets: order_subtotal_target(),
            message: Some(format!("{}% volume discount", percentage)),
        }],
        ..Default::default()
    })
}

The pattern is the same whatever the rule. The Function reads the cart, decides, and returns discount operations or nothing. The detail that catches people out is discount combination. A Function discount can be configured to combine, or not combine, with your other product, order and shipping discounts, and getting that setting wrong is the most common way a Function discount surprises a merchant by either stacking too generously or refusing to apply alongside a code. It is worth deciding the combination behaviour deliberately, not by default.

For a small store, a discount Function earns its cost when the rule is part of how the shop sells, a wholesale-style tier or a standing bundle promotion that will be reused for months. A one-off seasonal sale does not justify a build. Use a native discount for that and keep the Function for the logic that is genuinely yours.

Use-case two: a delivery customisation Function that hides the wrong shipping method

By default, Shopify shows every eligible shipping rate from your delivery profiles to every customer. That is fine until it is not. A store sells a heavy item and a customer picks express anyway, then complains when it costs a fortune or arrives slowly because express was never realistic for that weight. A store offers local pickup and out-of-area customers select it by mistake. A pre-order item ships in three weeks but express is still offered as though it will not. Each of these is a support ticket, a refund, or a quietly annoyed customer, and none of it is the customer's fault. They picked from what they were shown.

A delivery customisation Function runs at checkout and decides which delivery options the customer actually sees. It can hide an option, rename it, or reorder the list. Here is a JavaScript delivery customisation Function that hides any option whose title contains "Express" when the cart includes a line flagged as a pre-order.

export function run(input) {
  const hasPreorder = input.cart.lines.some(
    (line) => line.preorder?.value === "true"
  );

  if (!hasPreorder) {
    return { operations: [] };
  }

  const operations = input.cart.deliveryGroups
    .flatMap((group) => group.deliveryOptions)
    .filter((option) => option.title?.toLowerCase().includes("express"))
    .map((option) => ({
      hide: { deliveryOptionHandle: option.handle },
    }));

  return { operations };
}

The logic reads cleanly: if there is a pre-order line, hide every express option, otherwise leave the list alone. The same structure handles hiding courier delivery to PO Box addresses, hiding local pickup outside a postcode range, or pushing the cheapest standard option to the top of the list so it is the obvious default.

The worth-it test here is simple. If your store regularly gets contacted because customers chose a shipping method that should never have been on the menu, the Function pays for itself in saved support time and prevented refunds, and it does it quietly on every order from the day it goes live.

Use-case three: a payment customisation Function that controls risk at checkout

The payment customisation Function is the one I most often see overlooked, and it is genuinely useful for UK and US SMBs. By default, every payment method you configure is shown to every customer. A payment customisation Function lets you hide, rename or reorder those methods at checkout based on the cart total, the shipping address, the customer, or cart attributes.

The classic case is cash on delivery. COD is convenient, but on a high-value order it carries real risk: the customer can simply refuse the parcel and you have shipped goods for nothing. A Function can remove COD once the cart total crosses a threshold, while leaving it available on smaller, lower-risk orders. The same mechanism lets a B2B store hide a manual bank-transfer method from first-time customers, or reorder methods so the one you most want used sits at the top.

Here is a Rust payment customisation Function that hides a method named "Cash on Delivery" when the cart total goes above a set amount.

fn run(input: Input) -> Result<FunctionResult> {
    let threshold = 100.0;
    let total: f64 = input.cart.cost.total_amount.amount.parse()?;

    let operations = if total > threshold {
        input.payment_methods.iter()
            .filter(|method| method.name == "Cash on Delivery")
            .map(|method| Operation::Hide(HidePaymentMethod {
                payment_method_id: method.id.clone(),
            }))
            .collect()
    } else {
        vec![]
    };

    Ok(FunctionResult { operations })
}

Below the threshold the method stays, above it the method disappears. Hiding a risky payment route on exactly the orders where the risk is largest is a direct, measurable reduction in failed orders and chargeback exposure, and it does not nag the customer or add a step. They simply never see the option that should not have been there.

What a Function costs to build versus renting an app

This is the part most articles on Shopify Functions skip, so here is the honest version. For each of the three use-cases there are apps in the Shopify App Store: discount-logic apps, shipping-rule apps, payment-rule apps. They typically run somewhere from five to thirty dollars or more per month each. Rent three of them and you are paying a recurring annual cost in the low hundreds of dollars, every year, indefinitely, for logic that lives in software you do not own.

A Function is the other shape of cost. It is a single, contained build paid for once. A delivery or payment customisation Function that hides one method under one clear condition is genuinely a few hours of developer time, because the logic is small and the input query is narrow. A discount Function with tiered or bundle rules takes a little more, mostly because the rules and the discount-combination behaviour need careful testing rather than because the code is large. Once it is deployed there is no monthly fee for the Function itself. It runs on Shopify's infrastructure with no per-execution charge.

So the break-even is not abstract. A Function beats an app when three things are true. The rule is stable and not something you will rewrite every month. You would otherwise rent an app for that job for the foreseeable future. And the rule is specific enough to your store that no off-the-shelf app fits it cleanly anyway, which is often the real situation, since you end up paying for an app and still bending your rule to fit its settings.

When a Function is the wrong call

I would rather you not build a Function than build the wrong one, so here are the cases where an app or a native setting wins. If the rule changes constantly, a Function is a poor fit, because every change is a code deploy rather than a settings toggle, and you do not want to call a developer every fortnight. If a non-technical person on your team needs to edit the rule themselves, an app with a proper admin screen is the kinder choice. If a no-code app genuinely covers your exact need with no compromise, and the monthly fee is small, the convenience can be worth it. And for one-off promotions, reach for a native discount, not a build.

There is also a practical reality to accept. Functions run server-side at checkout, so you cannot eyeball them the way you tweak a theme. They need testing against real cart shapes, they are developed and deployed with the Shopify CLI and a Partner account, and they are designed to stay small and fast. None of that is hard, but it does mean a Function is a development task, not an afternoon of fiddling in the admin.

Where WitsCode comes in

WitsCode is a small web development agency, and a lot of what we do is the last mile for store owners and vibe coders who got most of the way on their own. Shopify Functions are a very common version of that last mile. We see Functions that an AI tool scaffolded and that compile fine but mishandle discount combination, so a code stacks with the Function discount when it should not. We see stores buried in shipping-method support tickets that one delivery customisation Function would quietly end. We see merchants paying for three rule apps that one or two Functions would replace.

If you read the three use-cases above and recognised a problem your store actually has, that is the conversation to have with us. We will scope the one rule, tell you honestly whether a Function or an app is the better answer for it, and if it is a Function we will build it inside a tidy custom app, test it against the cart shapes your store really sees, and hand it over working. It is a small, contained piece of work, and for the right rule on a small store it pays for itself and then keeps paying, quietly, on every order.

Get weekly field notes.

Practical writing on shipping products, straight to your inbox. No spam.

Need help with this?

WordPress Development

We design and build web apps, MVPs, and SaaS products. Talk to us about what you are working on.

Talk to us

Want to discuss shopify niche topics (gap-fill) for your business?

Start a project and we'll talk through where you are, what's working, and the highest-leverage moves for the next 90 days.