Fulfillment Constraints
fulfillment_constraints functions run at order placement and decide,
per cart line, which fulfillment locations are allowed to ship it.
If a line ends up with zero allowed locations the order is blocked
with a clear error. Non-blocking outputs (every line has at least one
allowed location) are persisted onto the order so the downstream routing
engine can pick the location without re-running the function.
Use it when:
- Some SKUs are only stocked at certain warehouses (single-source items).
- A line is hazmat and must ship from a licensed hub only.
- An express-shipping promise can only be honoured from specific regions.
- A line carries a made-to-order attribute and must route to the manufacturer.
- A line is digital and should bypass physical locations entirely.
fulfillment_constraints is the WASM-backed, code-driven counterpart to
order routing rules. Both narrow which
location ships which line; this one can also block the order.
How it works
The function runs at order placement immediately afterorder_validation and before the order row is persisted. If you
return an empty allowedLocationIds for any line, the entire order is
rejected — partial blocking is not supported.
Per-shop cap: 5 active fulfillment_constraints apps (matches
order_validation). The dispatcher runs every installed app of this
type and collects their constraints into a single union before
evaluating blocks.
Manifest
type must be fulfillment_constraints. entrypoint is the path
inside your app bundle to the compiled WASM. The inputFields
projection (see Input fields) trims the
payload to just the fields you read — recommended for fast dispatch.
Input shape
cart.items vs cart.lines. The platform serializes cart contents
as items for back-compat with existing apps; some examples in this doc
use lines (the canonical name in our input-field schema). Treat them
as the same array. If your function uses inputFields to project, list
cart.lines — the projector aliases both.Output shape
lineIdmust match acart.items[].idreturned by the platform. An unknown lineId is silently ignored.allowedLocationIdsis the narrowed set — locations your function approves. An empty array blocks the order.messageis only displayed when blocking. If omitted, the customer sees"Line <lineId> cannot be fulfilled from any location".
Validator
The platform validates output shape strictly:
A discarded result behaves as if the function returned no constraints —
the order proceeds. This is intentional: a buggy app must not be able to
take a merchant’s storefront offline.
Worked examples
Example: hazmat → licensed hub only
Preventing flammable / hazardous SKUs from shipping from a regular DC.hazmat-hub is configured for the shop and the catalogue is
empty, the function returns the hard-coded id and the routing engine
will use it. If the merchant later wires the catalogue, the function
auto-picks up the right ids without redeploy.
Example: oversold lines → block
Prevent a checkout when an in-cart line has zero stock anywhere. This is the canonical “fail-loud” use case.allowedLocationIds: [], the entire
order is blocked with HTTP 400 and all the function’s messages are
joined into a single human-readable string.
Example: geo-restricted SKUs
Some SKUs (knives, alcohol, electronics with regional certifications) are only legal to ship within certain regions.Example: digital lines bypass physical locations
Digital downloads should not consume a physical-location slot.Example: combine narrowing + non-blocking pass-through
A common pattern: narrow some lines (regional), block others (oversold), let the rest fall through unconstrained.Error behaviour when no eligible location exists
When any constraint resolves toallowedLocationIds: [], the order
is rejected with:
message is the concatenation of every blocking
constraint’s message, joined with "; ". The structured
data.errors[] array carries enough metadata for the checkout to surface
each reason inline next to the offending line.
data.code is always FulfillmentConstraintsFailed — clients can switch
on this to render a fulfilment-specific error UI instead of a generic
validation error.
Combination with order routing rules
When bothfulfillment_constraints and
order routing rules are active, constraints
run first:
fulfillment_constraintsevaluates → blocks (if any) or narrows.- Routing rules then evaluate over the narrowed set.
- The chosen location must be in the constraint’s
allowedLocationIdsfor that line.
allowedLocationIds in
insertion order.
This composition lets you split concerns cleanly:
- Use
fulfillment_constraintsfor hard physical limits (no inventory; unlicensed location). - Use routing rules for preferences (West Coast → Oakland; high-value → expedited).
Persistence
Non-blocking entries are merged onto the order atadditionalFields.fulfillmentConstraints[]:
<locations> by app <appId>” when the operations team
opens the order. The downstream routing engine reads it to make the
final location pick.
Performance considerations
- Project your input.
fulfillment_constraintsruns on every order placement. A function that reads onlycart.lines[].metafieldsshippingAddress.countryshould project to just those fields withinputFields— saves ~80% of dispatch time on large carts.
- Avoid network when possible. Network access
is gated behind
network_access: true. Each outbound call adds up to 1500 ms to checkout latency. If you need live inventory, cache it on the merchant side and refresh out of band. - Return early. If your function only affects a subset of lines, do
one pass over
cart.items, push constraints for lines you have an opinion on, and ignore the rest. Emptyconstraints: []is a valid output.
See also
- Order routing rules — declarative, non-blocking counterpart for region/SKU preference.
- Order validation functions — block orders before constraints run, based on cart-level rules.
- Cart transform functions — reshape lines before this function reads them.
- Input field selection — trim the input payload to just what you read.
- Network access — opt into outbound HTTP for live inventory lookups.