Keep pre-orders in a separate shipment

A Shopify fulfillment constraints Function that sends tagged pre-order items from a pre-order location and in-stock items from the others.

Function API
Fulfillment Constraints
Target
cart.fulfillment-constraints.generate.run
Plan
Any plan in a public app; Shopify Plus in a custom app
App scope
write_fulfillment_constraint_rules
API version
2026-10
Last checked

In short

This fulfillment constraints Function keeps pre-order items apart from in-stock items: products with your pre-order tag must ship from a dedicated location, and everything else from your other locations, so they don’t wait in one package. It uses the cart.fulfillment-constraints.generate.run target of the Fulfillment Constraints API, version 2026-10, on any plan in a public app.

How to use it

  1. Create a location for pre-order stock and tag pre-order products, for example pre-order.
  2. Create an app with Shopify CLI 4.9.0 or later, add the extension (shopify app generate extension --template fulfillment_constraints) and replace its files with the files below.
  3. Run the fixtures with shopify app function run --input tests/mixed-cart.input.json, then deploy.
  4. Activate it with fulfillmentConstraintRuleCreate, deliveryMethodTypes: [SHIPPING], and set {"preorderTags": ["pre-order"], "preorderLocationId": "gid://shopify/Location/1234567890"} in its metafields.

Code

shopify.extension.toml
api_version = "2026-10"

[[extensions]]
name = "Keep pre-orders in a separate shipment"
handle = "pre-orders-separate-shipment"
type = "function"

  [[extensions.targeting]]
  target = "cart.fulfillment-constraints.generate.run"
  input_query = "src/cart_fulfillment_constraints_generate_run.graphql"
  input_variables = { namespace = "$app", key = "function-configuration" }
  export = "cart-fulfillment-constraints-generate-run"

  [extensions.build]
  command = ""
  path = "dist/function.wasm"
src/cart_fulfillment_constraints_generate_run.graphql
query CartFulfillmentConstraintsGenerateRunInput($preorderTags: [String!]) {
  cart {
    deliverableLines {
      id
      merchandise {
        __typename
        ... on ProductVariant {
          product {
            hasAnyTag(tags: $preorderTags)
          }
        }
      }
    }
  }
  locations {
    id
  }
  fulfillmentConstraintRule {
    metafield(namespace: "$app", key: "function-configuration") {
      jsonValue
    }
  }
}
src/cart_fulfillment_constraints_generate_run.js
// @ts-check

/**
 * @typedef {import("../generated/api").CartFulfillmentConstraintsGenerateRunInput} CartFulfillmentConstraintsGenerateRunInput
 * @typedef {import("../generated/api").CartFulfillmentConstraintsGenerateRunResult} CartFulfillmentConstraintsGenerateRunResult
 */

/**
 * Settings stored as JSON in the rule's metafield. `preorderTags` in the same
 * JSON feeds the query variable (see input_variables in the toml), e.g.
 * { "preorderTags": ["pre-order"], "preorderLocationId": "gid://shopify/Location/1234567890" }.
 */
const DEFAULT_CONFIG = {
  // The location that holds (or will receive) pre-order stock.
  preorderLocationId: /** @type {string | null} */ (null),
};

/** @type {CartFulfillmentConstraintsGenerateRunResult} */
const NO_CONSTRAINTS = { operations: [] };

/**
 * @param {CartFulfillmentConstraintsGenerateRunInput} input
 * @returns {CartFulfillmentConstraintsGenerateRunResult}
 */
export function cartFulfillmentConstraintsGenerateRun(input) {
  const config = { ...DEFAULT_CONFIG, ...input.fulfillmentConstraintRule?.metafield?.jsonValue };
  const preorderLocationId = config.preorderLocationId;
  if (!preorderLocationId) return NO_CONSTRAINTS;

  const isPreorder = (/** @type {(typeof input.cart.deliverableLines)[number]} */ line) =>
    line.merchandise.__typename === "ProductVariant" && line.merchandise.product.hasAnyTag;
  const preorderLineIds = input.cart.deliverableLines.filter(isPreorder).map((line) => line.id);
  const inStockLineIds = input.cart.deliverableLines.filter((line) => !isPreorder(line)).map((line) => line.id);
  if (preorderLineIds.length === 0) return NO_CONSTRAINTS;

  const operations = [
    { deliverableLinesMustFulfillFromAdd: { deliverableLineIds: preorderLineIds, locationIds: [preorderLocationId] } },
  ];

  // Keeping the in-stock items away from the pre-order location is what
  // splits the order: they can no longer wait in the same package.
  const otherLocationIds = input.locations.map((location) => location.id).filter((id) => id !== preorderLocationId);
  if (inStockLineIds.length > 0 && otherLocationIds.length > 0) {
    operations.push({
      deliverableLinesMustFulfillFromAdd: { deliverableLineIds: inStockLineIds, locationIds: otherLocationIds },
    });
  }

  return { operations };
}
Test input and expected output
tests/mixed-cart.input.json
{
  "cart": {
    "deliverableLines": [
      { "id": "gid://shopify/DeliverableCartLine/1", "merchandise": { "__typename": "ProductVariant", "product": { "hasAnyTag": false } } },
      { "id": "gid://shopify/DeliverableCartLine/2", "merchandise": { "__typename": "ProductVariant", "product": { "hasAnyTag": true } } }
    ]
  },
  "locations": [
    { "id": "gid://shopify/Location/1111111111" },
    { "id": "gid://shopify/Location/2222222222" },
    { "id": "gid://shopify/Location/3333333333" }
  ],
  "fulfillmentConstraintRule": {
    "metafield": {
      "jsonValue": { "preorderTags": ["pre-order"], "preorderLocationId": "gid://shopify/Location/3333333333" }
    }
  }
}
tests/mixed-cart.output.json
{
  "operations": [
    {
      "deliverableLinesMustFulfillFromAdd": {
        "deliverableLineIds": ["gid://shopify/DeliverableCartLine/2"],
        "locationIds": ["gid://shopify/Location/3333333333"]
      }
    },
    {
      "deliverableLinesMustFulfillFromAdd": {
        "deliverableLineIds": ["gid://shopify/DeliverableCartLine/1"],
        "locationIds": ["gid://shopify/Location/1111111111", "gid://shopify/Location/2222222222"]
      }
    }
  ]
}
tests/no-location-configured.input.json
{
  "cart": {
    "deliverableLines": [
      { "id": "gid://shopify/DeliverableCartLine/1", "merchandise": { "__typename": "ProductVariant", "product": { "hasAnyTag": true } } }
    ]
  },
  "locations": [{ "id": "gid://shopify/Location/1111111111" }],
  "fulfillmentConstraintRule": { "metafield": null }
}
tests/no-location-configured.output.json
{ "operations": [] }
tests/only-in-stock-items.input.json
{
  "cart": {
    "deliverableLines": [
      { "id": "gid://shopify/DeliverableCartLine/1", "merchandise": { "__typename": "ProductVariant", "product": { "hasAnyTag": false } } }
    ]
  },
  "locations": [
    { "id": "gid://shopify/Location/1111111111" },
    { "id": "gid://shopify/Location/2222222222" },
    { "id": "gid://shopify/Location/3333333333" }
  ],
  "fulfillmentConstraintRule": {
    "metafield": {
      "jsonValue": { "preorderTags": ["pre-order"], "preorderLocationId": "gid://shopify/Location/3333333333" }
    }
  }
}
tests/only-in-stock-items.output.json
{ "operations": [] }

Written by Lintel. MIT license: free to use in your own store and in client work.

Limits and edge cases

  • If a constrained item isn’t stocked at an allowed location, checkout shows no shipping rates and can’t be completed. Keep pre-order inventory at the pre-order location.
  • Mutually exclusive constraints also remove all shipping rates; test carts with every product type.
  • The rule shows under Settings › Shipping and delivery › Order routing with the Function’s title and description.
  • A store can activate up to 25 fulfillment constraint Functions.

Questions

How do I ship pre-orders separately from in-stock items on Shopify?
Stock pre-orders at their own location and use a fulfillment constraints Function that ties pre-order lines to that location and other lines to the rest, as this example does.
Does this also apply to local pickup?
Only to the delivery method types you pass to fulfillmentConstraintRuleCreate. This example uses SHIPPING, so pickup orders aren’t constrained.
Can I detect pre-orders without tags?
Deliverable lines don’t expose selling plans, so this example uses a product tag. A variant metafield works too if you prefer.
  • Written for Function API version 2026-10 and checked against Shopify’s reference on the date shown. Shopify releases a new version every quarter.
  • Custom apps that contain Functions only run on Shopify Plus stores; a public app with the same Function runs on any plan unless the page says otherwise.
  • Every example ships with test fixtures, and we run them before publishing. Run them again after you change the code: shopify app function run.
  • MIT license. Use the code in your own store or in client work.

Need this built, tested and maintained for your store? See Shopify Functions development.

Have something to build? Let’s talk it through.

Taking a limited number of projects at a time. Every brief is read by an engineer, not a salesperson.

Gencer KarakayaShopify Engineer

An engineer reads your brief, and we reply by email.