Skip to main content

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

Order routing rules are declarative manifests that match cart lines to fulfillment locations. Unlike 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).
Status — install accepted, runtime not yet wired.The install pipeline accepts and persists routing-rule manifests today (under public/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 wired in order.service.ts. The only fulfillment-routing logic that fires today is fulfillment_constraints, which runs inside addClientOrder 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.A backend FunctionType.FULFILLMENT_LOCATION_RULE enum entry needs to be added alongside the evaluator. The CustomerLMS install path is ready; the backend dispatcher will pick rules up once the enum entry exists.

When to use rules vs constraints

Use a routing rule when…Use a fulfillment_constraints function when…
Assignment is a fixed lookup (countrylocationId).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:
public/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

{
  "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

FieldRequiredDescription
handleyesURL-safe rule id, unique per app. Used as the schema filename.
titleyesHuman label shown in the merchant’s fulfilment dashboard.
typenoAlways "fulfillment_location_rule". Defaults to this if omitted.
rule.matchyesMatcher object. See Match operators.
rule.assignyesAssignment block. See Assignment.
rule.fallbacknoBehaviour when no rule in the rule set matches. See 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

FormMeaningExample
"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:
{
  "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

PathTypeNotes
shippingAddress.countrystring (ISO-3166-1 alpha-2)"US", "CA", "GB"
shippingAddress.provincestring (e.g. "CA")State / region code.
shippingAddress.citystring
shippingAddress.zipstringUseful with startsWith for ZIP prefixes.
cart.totalPricenumberSubtotal in display currency.
cart.itemCountnumberSum of quantities.
cart.currencystring"USD", "INR", etc.
cart.lines[].merchandise.skustring (per line)Project over every line.
cart.lines[].merchandise.productIdstring (per line)
cart.lines[].merchandise.attributes.<key>string (per line)Free-form per-line attributes (hazmat, oversized, …).
cart.lines[].quantitynumber (per line)
customer.tagsstring[]Use contains for tag membership.
customer.idstring
Array projections ([]) match if any line satisfies the inner predicate. To require all lines to match, wrap in all:
{
  "match": {
    "all": [
      { "cart.lines[].merchandise.attributes.hazmat": "true" }
    ]
  }
}

Assignment

{
  "assign": {
    "locationId": "gid://launchmystore/Location/oakland-dc",
    "priority": 10,
    "fallback": false
  }
}
FieldRequiredDescription
locationIdyesTarget fulfilment location. Format: gid://launchmystore/Location/<uuid>.
prioritynoTie-breaker (default 0). The highest-priority match in the rule set wins.
fallbacknoIf 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:
{
  "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

{
  "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

{
  "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

{
  "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

{
  "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

{
  "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:
{
  "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: 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