> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Order Routing Rules

> Declarative rules that decide which fulfillment location ships which lines

# Order Routing Rules

Order routing rules are **declarative manifests** that match cart lines
to fulfillment locations. Unlike [`fulfillment_constraints`](/functions/fulfillment-constraints)
functions (which run WASM and can block order placement), routing rules
are pure JSON matchers evaluated by the platform at order placement.

Use routing rules when the assignment is **data-driven** (SKU prefix,
tag, country, line attribute) and `fulfillment_constraints` when you
need **code** (call your inventory service, look up real-time stock,
hit a 3PL API).

<Warning>
  **Status — install accepted, runtime not yet wired.**

  The install pipeline accepts and persists routing-rule manifests today
  (under `extensions/<domainSlug>/<app-handle>/order-routing-rules/`).
  They surface through the local manifest reader as the
  `fulfillment_location_rule` function type. The per-shop cap is enforced.

  However, the runtime evaluator that consults these rules at order
  placement is **not yet live**. The only
  fulfillment-routing logic that fires today is
  [`fulfillment_constraints`](/functions/fulfillment-constraints), which
  runs at order placement after `order_validation`. Until the rule
  evaluator lands, install routing rules for forward compatibility and
  rely on `fulfillment_constraints` for any logic you need to enforce.
</Warning>

## When to use rules vs constraints

| Use a routing rule when…                                 | Use a `fulfillment_constraints` function when…                              |
| -------------------------------------------------------- | --------------------------------------------------------------------------- |
| Assignment is a fixed lookup (`country` → `locationId`). | You need to call an external service to decide.                             |
| Logic is JSON-expressible (`startsWith`, `in`, `gt`).    | Logic needs imperative code (loops over inventory, joins, computed scores). |
| You want a non-developer to author the rule.             | You're shipping WASM anyway.                                                |
| You're OK with order-time evaluation only.               | You also need to *block* orders.                                            |

The two are complementary. A typical setup:

1. `fulfillment_constraints` narrows the set of locations a line **can**
   ship from (block if zero, otherwise narrow to the eligible set).
2. Routing rules pick the **preferred** location from that narrowed set
   (West Coast → Oakland; everything else → New Jersey).

When both run, **constraints execute first**; the rule evaluator only
sees lines that survived constraint blocking.

## Where rules live

Each rule is one entry in `extensions.orderRoutingRules` on your
`app.json`. After install, each entry is persisted as a single schema
file at:

```
extensions/{domainSlug}/{appHandle}/order-routing-rules/{handle}.schema.json
```

The local manifest reader flattens these into the canonical function
shape with `type: 'fulfillment_location_rule'`, so the dispatcher can
consume a single union of all routing extensions across all installed
apps. Per-shop cap: **25** active rules across all apps.

## Manifest

```json theme={null}
{
  "handle": "fulfillment-router",
  "name": "Fulfilment Router",
  "version": "1.0.0",
  "extensions": {
    "orderRoutingRules": [
      {
        "handle": "us-west-warehouse",
        "title": "Route West Coast US to Oakland",
        "type": "fulfillment_location_rule",
        "rule": {
          "match": {
            "shippingAddress.country": "US",
            "shippingAddress.province": ["CA", "OR", "WA", "NV"]
          },
          "assign": {
            "locationId": "gid://launchmystore/Location/oakland-dc",
            "priority": 10
          }
        }
      }
    ]
  }
}
```

### Fields

| Field           | Required | Description                                                                |
| --------------- | -------- | -------------------------------------------------------------------------- |
| `handle`        | yes      | URL-safe rule id, unique per app. Used as the schema filename.             |
| `title`         | yes      | Human label shown in the merchant's fulfilment dashboard.                  |
| `type`          | no       | Always `"fulfillment_location_rule"`. Defaults to this if omitted.         |
| `rule.match`    | yes      | Matcher object. See [Match operators](#match-operators).                   |
| `rule.assign`   | yes      | Assignment block. See [Assignment](#assignment).                           |
| `rule.fallback` | no       | Behaviour when no rule in the rule set matches. See [Fallback](#fallback). |

## Match operators

Paths use dotted notation against the cart input. A path can be a
literal field name or include `[]` to project across an array.

### Full operator list

| Form                          | Meaning                                     | Example                                    |
| ----------------------------- | ------------------------------------------- | ------------------------------------------ |
| `"path": "value"`             | Exact equality.                             | `"shippingAddress.country": "US"`          |
| `"path": ["v1", "v2"]`        | Any-of (the value is one of these).         | `"shippingAddress.province": ["CA", "WA"]` |
| `"path": { "equals": v }`     | Same as `"path": v`, explicit form.         | `{ "equals": "FRAGILE" }`                  |
| `"path": { "in": [v1, v2] }`  | Same as `"path": [v1, v2]`, explicit form.  | `{ "in": ["CA", "NV"] }`                   |
| `"path": { "gt": n }`         | Numeric: strictly greater than.             | `"cart.totalPrice": { "gt": 100 }`         |
| `"path": { "gte": n }`        | Numeric: greater than or equal.             | `{ "gte": 50 }`                            |
| `"path": { "lt": n }`         | Numeric: strictly less than.                | `{ "lt": 250 }`                            |
| `"path": { "lte": n }`        | Numeric: less than or equal.                | `{ "lte": 5 }`                             |
| `"path": { "startsWith": s }` | String prefix match.                        | `{ "startsWith": "ABC-" }`                 |
| `"path": { "endsWith": s }`   | String suffix match.                        | `{ "endsWith": "-XL" }`                    |
| `"path": { "contains": s }`   | Substring (strings) or `includes` (arrays). | `{ "contains": "fragile" }`                |
| `"path": { "not": <op> }`     | Negate any of the above.                    | `{ "not": { "in": ["CA"] } }`              |

### Boolean composition

Multiple keys in the same `match` block are **ANDed**. For explicit
boolean logic, use `any` (OR) or `all` (AND) lists at the top level:

```json theme={null}
{
  "match": {
    "any": [
      { "shippingAddress.country": "US" },
      { "shippingAddress.country": "CA" }
    ],
    "cart.totalPrice": { "gte": 50 }
  }
}
```

The above matches when `(country == US OR country == CA) AND totalPrice >= 50`.

### Common paths

| Path                                        | Type                        | Notes                                                     |
| ------------------------------------------- | --------------------------- | --------------------------------------------------------- |
| `shippingAddress.country`                   | string (ISO-3166-1 alpha-2) | `"US"`, `"CA"`, `"GB"`                                    |
| `shippingAddress.province`                  | string (e.g. `"CA"`)        | State / region code.                                      |
| `shippingAddress.city`                      | string                      |                                                           |
| `shippingAddress.zip`                       | string                      | Useful with `startsWith` for ZIP prefixes.                |
| `cart.totalPrice`                           | number                      | Subtotal in display currency.                             |
| `cart.itemCount`                            | number                      | Sum of quantities.                                        |
| `cart.currency`                             | string                      | `"USD"`, `"INR"`, etc.                                    |
| `cart.lines[].merchandise.sku`              | string (per line)           | Project over every line.                                  |
| `cart.lines[].merchandise.productId`        | string (per line)           |                                                           |
| `cart.lines[].merchandise.attributes.<key>` | string (per line)           | Free-form per-line attributes (`hazmat`, `oversized`, …). |
| `cart.lines[].quantity`                     | number (per line)           |                                                           |
| `customer.tags`                             | string\[]                   | Use `contains` for tag membership.                        |
| `customer.id`                               | string                      |                                                           |

Array projections (`[]`) match if **any** line satisfies the inner
predicate. To require **all** lines to match, wrap in `all`:

```json theme={null}
{
  "match": {
    "all": [
      { "cart.lines[].merchandise.attributes.hazmat": "true" }
    ]
  }
}
```

## Assignment

```json theme={null}
{
  "assign": {
    "locationId": "gid://launchmystore/Location/oakland-dc",
    "priority": 10,
    "fallback": false
  }
}
```

| Field        | Required | Description                                                                     |
| ------------ | -------- | ------------------------------------------------------------------------------- |
| `locationId` | yes      | Target fulfilment location. Format: `gid://launchmystore/Location/<uuid>`.      |
| `priority`   | no       | Tie-breaker (default `0`). The highest-priority match in the rule set wins.     |
| `fallback`   | no       | If `true`, only applies when no other rule matched. Useful for catch-all rules. |

When several rules match, the platform picks the one with the highest
`priority`. Ties are broken by **declaration order across apps** — there
is no defined cross-app ordering today, so prefer unambiguous priorities
when two apps might match the same line.

## Fallback

A catch-all rule:

```json theme={null}
{
  "handle": "catch-all-east-coast",
  "title": "Default to East Coast DC",
  "rule": {
    "match": {},
    "assign": {
      "locationId": "gid://launchmystore/Location/newark-dc",
      "priority": 0,
      "fallback": true
    }
  }
}
```

An empty `match: {}` matches anything. The `fallback: true` flag tells
the evaluator to only consider this rule when nothing else matched. With
fallbacks, you don't need to enumerate every possible region — write the
specifics, then catch the rest.

## Worked examples

### West Coast US → Oakland; everything else → Newark

```json theme={null}
{
  "extensions": {
    "orderRoutingRules": [
      {
        "handle": "us-west",
        "title": "West Coast US → Oakland",
        "rule": {
          "match": {
            "shippingAddress.country": "US",
            "shippingAddress.province": ["CA", "OR", "WA", "NV"]
          },
          "assign": {
            "locationId": "gid://launchmystore/Location/oakland-dc",
            "priority": 10
          }
        }
      },
      {
        "handle": "us-default",
        "title": "US default → Newark",
        "rule": {
          "match": { "shippingAddress.country": "US" },
          "assign": {
            "locationId": "gid://launchmystore/Location/newark-dc",
            "priority": 5,
            "fallback": true
          }
        }
      }
    ]
  }
}
```

Logic: a Californian order matches both rules. `us-west` has
`priority: 10` and wins. An Idaho order only matches `us-default` and
ships from Newark.

### Hazmat → licensed hub regardless of region

```json theme={null}
{
  "handle": "hazmat-routing",
  "title": "Hazmat ships from licensed hub",
  "rule": {
    "match": {
      "cart.lines[].merchandise.attributes.hazmat": "true"
    },
    "assign": {
      "locationId": "gid://launchmystore/Location/hazmat-hub",
      "priority": 100
    }
  }
}
```

`priority: 100` outranks the regional rules above. Any cart containing a
hazmat line is routed to the hazmat hub, ignoring shipping address.

### International → 3PL

```json theme={null}
{
  "handle": "international-3pl",
  "title": "International → DHL 3PL",
  "rule": {
    "match": {
      "shippingAddress.country": { "not": { "in": ["US", "CA"] } }
    },
    "assign": {
      "locationId": "gid://launchmystore/Location/dhl-3pl",
      "priority": 50
    }
  }
}
```

Anything outside the US and Canada goes to DHL's 3PL. This rule will be
beaten by the hazmat rule above (priority 100), so an international
hazmat order still goes to the hazmat hub.

### Backorder → drop-shipper

```json theme={null}
{
  "handle": "backorder-dropship",
  "title": "Backorder lines → drop-shipper",
  "rule": {
    "match": {
      "cart.lines[].merchandise.attributes.inventory_state": "backorder"
    },
    "assign": {
      "locationId": "gid://launchmystore/Location/dropshipper",
      "priority": 200
    }
  }
}
```

A line whose `inventory_state` attribute is `"backorder"` (typically
populated by your inventory app via a `cart_transform` function) is sent
straight to the drop-shipper, bypassing all other routing.

### High-value orders → expedited centre

```json theme={null}
{
  "handle": "high-value-expedited",
  "title": "High-value orders → expedited centre",
  "rule": {
    "match": {
      "cart.totalPrice": { "gt": 500 },
      "shippingAddress.country": "US"
    },
    "assign": {
      "locationId": "gid://launchmystore/Location/expedited-dc",
      "priority": 75
    }
  }
}
```

## Inspection

Once the runtime evaluator is wired, the result of routing will be
persisted on the order's `additionalFields.orderRouting`:

```json theme={null}
{
  "additionalFields": {
    "orderRouting": [
      {
        "lineId": "cl_abc...",
        "locationId": "gid://launchmystore/Location/oakland-dc",
        "matchedRule": "us-west",
        "matchedAppHandle": "fulfillment-router",
        "priority": 10
      },
      {
        "lineId": "cl_def...",
        "locationId": "gid://launchmystore/Location/hazmat-hub",
        "matchedRule": "hazmat-routing",
        "matchedAppHandle": "fulfillment-router",
        "priority": 100
      }
    ]
  }
}
```

This is the audit trail the fulfilment dashboard reads to show *"this
line goes to Oakland because rule `us-west` matched at priority 10"*.
A line that has **no matching rule** is omitted from the array — the
merchant's manual fulfilment workflow handles it.

## Interaction with `fulfillment_constraints`

When both routing rules and a `fulfillment_constraints` function are
installed for the same store, the platform runs them in this order:

```mermaid theme={null}
sequenceDiagram
    participant Checkout
    participant OrderSvc as Order Service
    participant Constraint as fulfillment_constraints fn
    participant Router as Routing rule evaluator

    Checkout->>OrderSvc: Place order
    OrderSvc->>Constraint: dispatch (per line)
    alt any line has empty allowedLocationIds
      Constraint-->>OrderSvc: blocking constraint
      OrderSvc-->>Checkout: HTTP 400 (FulfillmentConstraintsFailed)
    else all lines have at least one allowed location
      Constraint-->>OrderSvc: narrowed feasible set
      OrderSvc->>Router: evaluate rules over the narrowed set
      Router-->>OrderSvc: orderRouting[]
      OrderSvc-->>Checkout: order placed
    end
```

The rule evaluator **respects the narrowed set**: a rule that assigns
`oakland-dc` to a line whose constraint allows only `[newark-dc]` is
ignored for that line. Use `fulfillment_constraints` to enforce
hard physical limits (a location is the only one with stock) and use
routing rules to express preferences within the feasible set.

## Per-shop cap

A store can have up to **25** active `fulfillment_location_rule`
extensions across all installed apps. The cap matches the
`FUNCTION_ACTIVE_LIMITS[DISCOUNT]` entry — it's the loosest cap in the
function-types table because routing rules are non-blocking and pure
JSON. Install fails with HTTP 400 if the cap would be exceeded.

## See also

* [Fulfillment Constraints](/functions/fulfillment-constraints) — the
  WASM-backed counterpart that can block orders.
* [Cart Transform](/functions/cart-transform) — populate
  `cart.lines[].merchandise.attributes` so routing rules have something
  to match on.
* [App Manifest](/extensions/app-manifest) — full top-level manifest
  schema.
