How to Block a Shopify Checkout with a Cart and Checkout Validation Function
A developer walkthrough of Shopify's Cart and Checkout Validation Function: the input and output shape, how to return a validation error to block an order, the execution budget, and the metafield pre-stage pattern for live data like margin.
If you want to stop an order from completing on Shopify, the modern, supported primitive is the Cart and Checkout Validation Function. It replaces the old Ruby checkout Scripts (frozen for editing on April 15, 2026, fully shut down on June 30, 2026) and runs as a Shopify Function: a small WebAssembly module Shopify executes server-side during cart and checkout. This guide walks the input and output shape, how to actually block an order, the execution budget you have to respect, and the metafield pre-stage pattern that lets you bring live data such as real-time margin into a function that has almost no time to compute.
By Herzel Mishel, Founder of Agentis · Last updated May 29, 2026
What the function is
A Cart and Checkout Validation Function is server-side validation logic that Shopify runs as the buyer moves through the cart and checkout. It receives a structured snapshot of the cart and returns a list of validation errors. If you return any errors, Shopify surfaces them to the buyer and prevents checkout from progressing. If you return an empty list, the order proceeds. See the Cart and Checkout Validation Function glossary entry for the conceptual overview; this post is the implementation view.
The input: a GraphQL query over the cart
Each Function declares a GraphQL input query against Shopify's Function input schema. You request exactly the fields you need and nothing more, because the input you pull counts against your execution budget. A typical validation query asks for cart lines, quantities, the merchandise (variant) on each line, cost or price fields, and any metafields you staged ahead of time.
query Input {
cart {
lines {
quantity
cost {
totalAmount { amount currencyCode }
}
merchandise {
... on ProductVariant {
id
product {
# a metafield you wrote ahead of time (see pre-stage pattern)
floor: metafield(namespace: "margin", key: "floor") { value }
}
}
}
}
}
}
The output: return validation errors to block
The function returns an object containing an errors array. Each error has a human-readable message and a target that points at the part of the cart the error relates to. A non-empty array blocks checkout; an empty array allows it. The control flow is deliberately simple: decide, then either push an error or do not.
// run.js: the function entrypoint
export function run(input) {
const errors = [];
for (const line of input.cart.lines) {
const floorRaw = line.merchandise?.product?.floor?.value;
const lineTotal = Number(line.cost?.totalAmount?.amount ?? 0);
const floor = floorRaw ? Number(floorRaw) : null;
// Block the order if this line's resolved total is below the staged floor.
if (floor !== null && lineTotal < floor) {
errors.push({
localizedMessage: "This order cannot be completed at the current price.",
target: "$.cart",
});
}
}
return { errors };
}
That is the entire blocking mechanism. The hard part is never the errors.push(). It is having a trustworthy number to compare against, which is what the rest of this post is about.
The budget you cannot ignore: roughly 5ms and 64KB of Wasm
Shopify Functions run inside a tightly bounded sandbox. You get on the order of 5 milliseconds of execution time, and the compiled Wasm module has to fit inside a 64 kilobyte limit. There is no network access from inside a Function. You cannot call your ERP, hit a pricing API, or fetch an FX rate at evaluation time. Anything the function needs to know has to already be present in the input it queries.
This constraint is the single most important thing to internalize, because it quietly rules out the naive design. You cannot "look up the live cost of this SKU" inside the function. The function is fast and isolated precisely because it is not allowed to do slow, networked things. So the live data has to arrive some other way.
The metafield pre-stage pattern
The way you give a Function live-ish data is to write that data to a metafield before checkout happens, then read the metafield inside the function. The function reads from local input (fast and allowed); the freshness problem is pushed out to whatever process writes the metafield (which can be as slow and networked as it needs to be).
The pattern has three moving parts:
- An out-of-band writer. A service computes the value you need (say, the current landed-cost floor for each variant) using whatever live sources it wants: ERP cost, freight zone tables, FX rates. It runs on its own cadence, outside the checkout path.
- A metafield as the handoff. The writer stores the computed value on the product or variant as a metafield. This is now plain data sitting on the resource.
- The function reads the metafield. At checkout, the validation function queries that metafield in its input and compares it to the resolved cart. No network, no computation beyond a comparison, comfortably inside budget.
This pattern is robust, but it surfaces the real engineering problem: the value is only as good as the writer keeps it. A floor metafield written from last week's COGS will happily approve an order that is underwater today. The function is correct; the data is stale. Freshness, source-of-truth reconciliation, and write throughput across thousands of variants are now your problem.
Why margin is the rule that matters
You can validate plenty of things with this primitive: minimum order quantities, restricted shipping destinations, banned SKU combinations. Those are static rules with static data, and they are easy. The validation that actually protects the business is the one that asks whether this specific order, at its fully discounted price, still clears your profit floor.
That rule is hard for exactly the reason the budget section spelled out. The number you need to compare against (true landed margin right now) depends on live inputs the function cannot fetch: current COGS, the real freight cost to this destination, today's FX on a cross-border order, and the combined effect of every discount the buyer stacked. The validation function is a perfect place to enforce the floor. It is a terrible place to compute it. The enforcement primitive is the easy 20 percent; the live-cost data layer behind it is the hard 80 percent.
Where Agentis fits
Agentis is the live-cost data layer for exactly this pattern. It pulls real-time COGS from NetSuite, models actual freight and current FX, and keeps the per-variant profit-floor data staged so a checkout validation function has a trustworthy number to decide on, in under 10 milliseconds, without your team owning the reconciliation and freshness machinery. In other words, Agentis runs the hard 80 percent so the validation function can do the easy, correct thing: block the order that breaches the floor.
If you are building this yourself, the validation function is the right primitive and the metafield pre-stage pattern is the right shape. Just budget for the data layer, not the function, as the real work. To see the enforcement outcomes mapped to Shopify capabilities, read Block Checkout Orders on Shopify and Custom Checkout Rules for Shopify.
Summary
- The Cart and Checkout Validation Function blocks an order by returning a non-empty
errorsarray from itsrunentrypoint. - It runs in a sandbox with roughly 5ms of execution and a 64KB Wasm limit, and has no network access.
- Live data reaches the function through the metafield pre-stage pattern: an out-of-band writer computes a value, stores it on a metafield, and the function reads it.
- Margin is the rule worth enforcing, and the function is the place to enforce it, not the place to compute it. The live-cost data layer is the hard part, which is what Agentis provides.