# 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.

Page: https://lintelagency.com/resources/shopify-functions/keep-pre-orders-in-separate-shipment

## 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.

## Facts

- **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:** 2 November 2026

## 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

```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

```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

```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

```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

```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

```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

```json
{ "operations": [] }
```

### tests/only-in-stock-items.input.json

```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

```json
{ "operations": [] }
```

## 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.

## Shopify documentation

- [Fulfillment Constraints Function API](https://shopify.dev/docs/api/functions/latest/fulfillment-constraints)
- [Variables in input queries](https://shopify.dev/docs/apps/build/functions/input-queries/use-variables-input-queries)

## Related

- [Keeping inventory in sync with another system](https://lintelagency.com/journal/sync-inventory-external-system)
- [Which Shopify Functions capabilities need Shopify Plus](https://lintelagency.com/journal/shopify-functions-plus-requirements)
- [Configuring Functions with metafields, so rules change without deploys](https://lintelagency.com/journal/configuring-functions-with-metafields)

License: MIT. Free to use in your own store and in client work.
