
Multi Warehouse
- 85 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Manage inventory across multiple warehouses with smart allocation rules, transfer orders between locations, and split-fulfillment routing.
About
Manages multi-warehouse inventory with allocation rules, inter-location transfer orders, and split-fulfillment routing. A developer uses it to fulfill orders efficiently across several stocking locations.
- Smart allocation rules and transfer orders between locations
- Split-fulfillment routing logic
Multi Warehouse by the numbers
- 85 all-time installs (skills.sh)
- Ranked #3,036 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill multi-warehouseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 85 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Manage inventory across multiple warehouses with smart allocation rules, transfer orders between locations, and split-fulfillment routing.
Files
Multi-Warehouse Inventory
Overview
Multi-warehouse inventory lets you stock products at multiple physical locations — warehouses, stores, 3PLs — and route each order to the location best positioned to fulfill it. Shopify and BigCommerce have multi-location inventory built in. WooCommerce needs a plugin. Only build a custom allocation engine if your routing logic (zone-based, cost-optimized, split fulfillment) exceeds what platform tools support.
When to Use This Skill
- When a merchant operates more than one warehouse, store, or 3PL (third-party logistics) provider
- When shipping cost is significant and routing orders from the nearest warehouse matters
- When stock imbalance between locations requires transfer orders to redistribute inventory
- When orders must sometimes be split across two warehouses because no single location has all items
Core Instructions
Step 1: Determine platform and choose the right tool
| Platform | Built-in Multi-Location | Recommended Extension |
|---|---|---|
| Shopify | Yes — up to 1,000 locations (Basic: 4, Shopify plan: 5, Advanced: 8) | ShipBob or Flexport for 3PL fulfillment routing; ShipHero for advanced routing rules |
| WooCommerce | No — requires a plugin | ATUM Multi-Inventory (add-on) or WooCommerce Multi-Location Inventory by Iconic |
| BigCommerce | Yes — Multi-Location Inventory app (free) | ShipStation for advanced routing; Whiplash or ShipBob for 3PL integration |
| Custom / Headless | Build allocation engine with proximity-first routing | Required when no platform tools meet routing and split-fulfillment requirements |
---
Step 2: Platform-specific setup
---
Shopify
Shopify has native multi-location inventory.
Set up locations:
1. Go to Settings → Locations → Add location 2. Add each warehouse, store, or fulfillment center 3. Enter the address (used for shipping rate calculation) 4. Assign products to each location — go to a product, click a variant, and set the quantity at each location
Configure fulfillment routing:
1. Go to Settings → Shipping and delivery → Shipping 2. Under Fulfillment from, set the priority order for your locations 3. Shopify routes orders to the first location in your priority list that has stock for all items 4. For more sophisticated routing (proximity-based, cost-optimized): install ShipHero or ShipBob from the App Store
Transfer orders between locations:
1. Go to Inventory → Transfers → Create transfer 2. Select the Origin location (where stock is coming from) 3. Select the Destination location 4. Add the products and quantities to transfer 5. When goods arrive, go to Transfers → [Transfer] → Accept items — Shopify automatically increments the destination and decrements the origin
3PL integration:
- For ShipBob: Install ShipBob from the App Store; ShipBob pulls orders from Shopify and routes to the nearest fulfillment center automatically
- For Flexport: Install the Flexport app; products and inventory sync bi-directionally
---
WooCommerce
WooCommerce requires a plugin for multi-location inventory.
Option A: ATUM Multi-Inventory (recommended)
1. Install ATUM Inventory Management for WooCommerce (free core) + ATUM Multi-Inventory add-on (paid) 2. In ATUM, go to Multi-Inventory → Locations → Add Location 3. For each product, go to the ATUM Multi-Inventory tab and assign quantities per location 4. Configure inventory selection rule (region-based, priority-based, or first-available)
Option B: WooCommerce Multi-Location Inventory by Iconic
1. Install the plugin 2. Go to WooCommerce → Multi-Location Inventory → Locations and add your warehouses 3. Edit each product and set per-location stock quantities 4. Configure fulfillment routing in the plugin settings
Transfer orders:
- ATUM Multi-Inventory includes a transfer order feature — go to ATUM → Inventory Transfers → New Transfer
- Set origin, destination, products, and quantities
- Mark as received to update stock automatically
---
BigCommerce
BigCommerce supports multi-location inventory via a free app.
Enable Multi-Location Inventory:
1. Go to Apps → Search "Multi-Location Inventory" 2. Install and launch the app 3. Add locations: Locations → Add location with name and address 4. Assign stock per product per location
Configure fulfillment routing:
1. In the Multi-Location Inventory app, go to Routing Rules 2. Set rules: ship from nearest, ship from cheapest, ship from location with most stock, etc. 3. BigCommerce generates split shipments automatically when an order can't be fulfilled from a single location
For 3PL integration:
- Install ShipStation from the BigCommerce App Marketplace
- ShipStation connects to your 3PL providers and routes based on your rules
---
Custom / Headless
For headless storefronts, build an allocation engine that selects the best location(s) for each order:
// lib/allocation.ts
import { haversineDistance } from './geo';
interface OrderItem { variantId: string; quantity: number; productName: string; }
interface AllocationResult {
type: 'single' | 'split';
fulfillments: { locationId: string; items: OrderItem[] }[];
}
export async function allocateOrder(orderItems: OrderItem[], shippingAddress: Address): Promise<AllocationResult> {
const locations = await db.locations.findMany({ where: { active: true } });
// Fetch inventory availability for all required variants at all locations
const inventory = await db.inventoryLevels.findMany({
where: { variantId: { in: orderItems.map(i => i.variantId) } },
});
const invMap = buildInventoryMap(inventory); // locationId -> variantId -> available
// Score locations by distance to shipping address (proximity-first routing)
const scored = locations
.map(loc => ({
...loc,
distanceKm: haversineDistance({ lat: loc.lat, lng: loc.lng }, { lat: shippingAddress.lat, lng: shippingAddress.lng }),
}))
.sort((a, b) => a.distanceKm - b.distanceKm);
// Prefer single-location fulfillment to avoid split shipments
for (const location of scored) {
const canFulfillAll = orderItems.every(item => (invMap[location.id]?.[item.variantId] ?? 0) >= item.quantity);
if (canFulfillAll) return { type: 'single', fulfillments: [{ locationId: location.id, items: orderItems }] };
}
// Fall back to split fulfillment — greedy assignment to nearest location with stock
const remaining = [...orderItems];
const fulfillments: AllocationResult['fulfillments'] = [];
for (const location of scored) {
const canFulfill = remaining.filter(item => (invMap[location.id]?.[item.variantId] ?? 0) >= item.quantity);
if (canFulfill.length > 0) {
fulfillments.push({ locationId: location.id, items: canFulfill });
canFulfill.forEach(item => remaining.splice(remaining.findIndex(r => r.variantId === item.variantId), 1));
}
if (remaining.length === 0) break;
}
if (remaining.length > 0) throw new Error('Cannot fulfill order — insufficient stock across all locations');
return { type: 'split', fulfillments };
}
// Transfer order management
export async function receiveTransferOrder(transferOrderId: string, receivedItems: { variantId: string; quantity: number }[]) {
const transfer = await db.transferOrders.findUnique({ where: { id: transferOrderId }, include: { items: true } });
await db.$transaction([
// Decrease on_hand + reserved at source
...receivedItems.map(item => db.inventoryLevels.update({
where: { variantId_locationId: { variantId: item.variantId, locationId: transfer.fromLocationId } },
data: { onHand: { decrement: item.quantity }, reserved: { decrement: item.quantity } },
})),
// Increase on_hand at destination
...receivedItems.map(item => db.inventoryLevels.upsert({
where: { variantId_locationId: { variantId: item.variantId, locationId: transfer.toLocationId } },
create: { variantId: item.variantId, locationId: transfer.toLocationId, onHand: item.quantity, reserved: 0 },
update: { onHand: { increment: item.quantity } },
})),
db.transferOrders.update({ where: { id: transferOrderId }, data: { status: 'received' } }),
]);
}---
Step 3: Configure split-shipment communication
When an order is split across two locations, customers must be informed before confirming their order.
Shopify: Shopify's native checkout shows "Ships from multiple locations" in the shipping options when a split is needed. Customize the messaging in Settings → Checkout → Checkout language.
WooCommerce: Display a notice in the cart/checkout when ATUM detects a split is needed. ATUM Multi-Inventory includes configurable messaging for split shipments.
In the order confirmation email: Include all fulfillment groups with their expected shipping dates. "Your order will arrive in 2 shipments" with item breakdowns reduces support tickets.
---
Step 4: Balance stock across locations
Chronic stock imbalances (one warehouse overstocked, another out of stock) increase transfer costs and split fulfillments. Run a weekly analysis:
Signs of imbalance:
- One location consistently has the same SKU at zero while another has excess
- More than 20% of orders require split fulfillment for the same SKU
- Transfer requests for the same product direction repeat every week
Action: Create a transfer order from the overstocked location to the understocked one before the next reorder cycle.
Shopify Stocky: Shows an "Inventory distribution" report that identifies imbalanced SKUs across locations.
Best Practices
- Prefer single-location fulfillment — split shipments increase cost and confusion; exhaust single-location options before splitting
- Use platform-native multi-location before buying an app — Shopify and BigCommerce handle the common case well; only add apps when routing logic is more complex than what the platform supports
- Model transfer orders as in-transit inventory — reduce available stock at the source when the transfer ships; don't increment the destination until goods arrive and are counted
- Inform customers of split shipments at checkout, not after purchase — finding out after payment feels deceptive
- Run stock-balancing analysis weekly — a 15-minute review of the distribution report prevents chronic imbalances that compound over time
Common Pitfalls
| Problem | Solution |
|---|---|
| Split fulfillment creates two shipping charges | Consolidate shipping cost at the order level; absorb the second shipment cost or notify the customer during checkout — never charge twice silently |
| Transfer received quantity differs from sent | Support partial receipt — record the actual received quantity per item and handle discrepancies (damaged in transit) separately |
| Inventory double-counted across locations | Each inventory record is location-scoped; when reporting total stock, always sum across locations explicitly |
| Location goes offline mid-fulfillment | Mark location as inactive; re-run allocation for unfulfilled orders assigned to that location |
| Customer confused about multiple tracking numbers | Send a separate tracking email per fulfillment with a clear note: "This is shipment 1 of 2 for your order" |
Related Skills
- @inventory-tracking
- @low-stock-alerts
- @catalog-import-export
{
"context": "Tests whether the agent implements the allocation algorithm correctly: placing geo utilities in lib/geo.js, using haversine distance with the correct Earth radius, scoring all active locations by proximity, preferring single-location fulfillment before falling back to a greedy split, and throwing the right error when stock is insufficient.",
"type": "weighted_checklist",
"checklist": [
{
"name": "geo module path",
"max_score": 6,
"description": "The distance utility is placed in lib/geo.js (not inline in allocation.js or another path)"
},
{
"name": "haversine formula used",
"max_score": 8,
"description": "lib/geo.js implements the haversine formula (contains sin, cos, atan2 or equivalent spherical trigonometry)"
},
{
"name": "Earth radius constant",
"max_score": 6,
"description": "The haversine implementation uses 6371 as the Earth radius (in km)"
},
{
"name": "allocation module path",
"max_score": 6,
"description": "The allocation logic is placed in lib/allocation.js"
},
{
"name": "haversine import",
"max_score": 8,
"description": "lib/allocation.js imports the distance function from './geo' (not a third-party library or inline copy)"
},
{
"name": "inventory map built",
"max_score": 8,
"description": "allocation.js builds a two-level map of locationId -> variantId -> available quantity before scoring locations"
},
{
"name": "proximity sort",
"max_score": 8,
"description": "Locations are scored by distance to the shipping address and sorted ascending (nearest first) before single or split attempts"
},
{
"name": "single-location tried first",
"max_score": 10,
"description": "The algorithm iterates sorted locations and attempts to fulfill the full order from one location before considering a split"
},
{
"name": "single result type",
"max_score": 8,
"description": "When a single location can fulfill the order, the return value has type: 'single'"
},
{
"name": "split fallback",
"max_score": 10,
"description": "When no single location can fulfill the order, the algorithm falls back to greedy split fulfillment (assigning items to nearest locations with stock)"
},
{
"name": "split result type",
"max_score": 8,
"description": "The split result has type: 'split' and a fulfillments array with one entry per contributing location"
},
{
"name": "AllocationError thrown",
"max_score": 8,
"description": "When total stock across all locations is insufficient to fulfill the order, an error is thrown (not a silent failure or null return)"
},
{
"name": "active filter",
"max_score": 6,
"description": "Only locations with active: true are considered during allocation"
}
]
}
Order Routing Engine for a Multi-Warehouse Retailer
Problem/Feature Description
A mid-size outdoor apparel company operates three fulfillment centers — one on the East Coast (New Jersey), one in the Midwest (Chicago), and one on the West Coast (Los Angeles). Historically their monolithic backend assigned every order to the East Coast warehouse regardless of where the customer lived, resulting in unnecessarily long transit times and high carrier costs for West Coast buyers.
The engineering team has been asked to replace the old fixed-assignment logic with a proper routing engine that sends each order to whichever warehouse can ship it most efficiently. The engine must handle the common case where a single warehouse can cover an entire order, and gracefully fall back when inventory is too spread out to fulfill from one place. The company also needs this component isolated and testable so it can be evaluated against historical order data.
Output Specification
Implement the allocation engine as JavaScript module(s). Use in-memory data structures rather than a real database (mock out any db calls). Produce:
- The core allocation logic as one or more JavaScript modules
allocation-demo.js— a runnable Node.js script (no external dependencies beyond Node built-ins) that:
1. Sets up a small set of warehouse locations with geographic coordinates 2. Defines a few test orders where: (a) one warehouse can cover the whole order, and (b) no single warehouse can and the order must be split 3. Calls the allocation function for each and prints the result (JSON) to stdout 4. Demonstrates the error case when stock is truly insufficient across all locations
Run the demo script and append its output as allocation-output.txt.
{
"context": "Tests whether the agent creates one fulfillment record per location with 'pending' status, reserves inventory after each fulfillment is created, and builds a SplitShipmentNotice component with the correct accessibility attribute, null return for single-location orders, and per-shipment item enumeration.",
"type": "weighted_checklist",
"checklist": [
{
"name": "one fulfillment per location",
"max_score": 10,
"description": "createFulfillments creates exactly one fulfillment record for each location in allocationResult.fulfillments (not one record for the whole order)"
},
{
"name": "pending initial status",
"max_score": 8,
"description": "Each fulfillment record is created with status 'pending'"
},
{
"name": "inventory reserved after creation",
"max_score": 10,
"description": "After creating fulfillment records, reserveInventory (or equivalent) is called for each item at its assigned location"
},
{
"name": "reserve includes referenceId",
"max_score": 7,
"description": "The reserveInventory call passes the fulfillment id as a referenceId (or equivalent reference field)"
},
{
"name": "role alert attribute",
"max_score": 10,
"description": "SplitShipmentNotice renders a container element with role='alert'"
},
{
"name": "null for single shipment",
"max_score": 9,
"description": "SplitShipmentNotice returns null (renders nothing) when fulfillments.length is 1 or less"
},
{
"name": "shipment count in message",
"max_score": 8,
"description": "The notice text includes the total number of shipments (e.g. 'will arrive in N separate shipments')"
},
{
"name": "per-shipment item list",
"max_score": 9,
"description": "The notice lists each shipment with its associated item names (one entry per fulfillment location)"
},
{
"name": "location key on items",
"max_score": 7,
"description": "The fulfillment list in SplitShipmentNotice uses locationId (or equivalent location identifier) as the React list key"
},
{
"name": "parallel fulfillment creation",
"max_score": 8,
"description": "createFulfillments creates all fulfillment records concurrently (Promise.all or equivalent) rather than sequentially"
},
{
"name": "fulfillment module path",
"max_score": 7,
"description": "The createFulfillments function is in lib/fulfillments.js"
},
{
"name": "split notice file path",
"max_score": 7,
"description": "The notification component is in SplitShipmentNotice.jsx"
}
]
}
Checkout Experience for Orders Shipped from Multiple Locations
Problem/Feature Description
An online furniture retailer recently expanded from one warehouse to three regional fulfillment centers. Their product catalog includes large items — sofas, dining sets, bed frames — where a single order might mix items that are only available at different locations. The current checkout flow was built assuming a single shipment per order; it silently books a second shipment without telling the customer, leading to confusion when two packages arrive days apart and the customer contacts support thinking an item is missing.
The product team wants two things fixed: first, when an order must ship from more than one location the checkout page should clearly tell the customer what to expect before they confirm the purchase; second, the backend should create proper per-location fulfillment records so each shipment can be tracked independently once the order is placed.
Output Specification
Produce the following files:
- A React component (functional, no external UI libraries needed) that renders the customer-facing notification for split shipments
- A JavaScript module that creates fulfillment records and reserves inventory; use in-memory data structures to simulate the database
fulfillment-demo.js— a runnable Node.js script (no external dependencies) that:
1. Simulates an allocation result with two locations each covering part of the order 2. Calls the fulfillment creation function and prints the resulting fulfillment records (JSON) to stdout 3. Prints the contents of the split shipment notice rendered to a simple HTML string (you may use a minimal string-serialisation approach rather than a full React renderer)
Run the demo script and save its output as fulfillment-output.txt.
{
"context": "Tests whether the agent correctly implements the three-phase transfer order lifecycle: draft creation with stock validation, shipping (reserve at source, status to in_transit), and receipt (decrement source on_hand+reserved, increment destination on_hand via upsert, status to received). Also checks atomic updates, partial receipt support, and correct module file placement.",
"type": "weighted_checklist",
"checklist": [
{
"name": "module file path",
"max_score": 5,
"description": "Transfer order logic is placed in api/admin/transfer-orders.js"
},
{
"name": "draft initial status",
"max_score": 8,
"description": "Newly created transfer orders have status 'draft' (not 'pending', 'open', or any other value)"
},
{
"name": "source stock validation",
"max_score": 9,
"description": "createTransferOrder checks that the source location's available quantity is >= the requested transfer quantity before creating the order"
},
{
"name": "validation error returned",
"max_score": 7,
"description": "When source stock is insufficient, a 400-equivalent error is returned (not a silent failure or exception swallowed)"
},
{
"name": "ship increments reserved",
"max_score": 10,
"description": "markTransferShipped increments the reserved count at the source location (does NOT decrement on_hand at this stage)"
},
{
"name": "in_transit status on ship",
"max_score": 7,
"description": "markTransferShipped updates the transfer order status to 'in_transit'"
},
{
"name": "receive decrements source",
"max_score": 10,
"description": "receiveTransferOrder decrements both on_hand and reserved at the source location for received quantities"
},
{
"name": "receive increments destination",
"max_score": 9,
"description": "receiveTransferOrder increments (or creates) on_hand at the destination location for received quantities"
},
{
"name": "received status on receipt",
"max_score": 7,
"description": "receiveTransferOrder updates the transfer order status to 'received'"
},
{
"name": "atomic update pattern",
"max_score": 9,
"description": "Ship and receive operations update inventory and order status together in a single transaction or equivalent atomic operation (not sequential independent writes)"
},
{
"name": "partial receipt support",
"max_score": 10,
"description": "receiveTransferOrder accepts a receivedItems parameter (or equivalent) that may contain quantities less than the original transfer, allowing partial receipt"
},
{
"name": "destination upsert",
"max_score": 9,
"description": "When receiving, the destination inventory row is created if it doesn't exist (upsert pattern — not just update)"
}
]
}
Stock Rebalancing System for Distributed Warehouses
Problem/Feature Description
A home goods retailer has noticed persistent regional stock imbalances: their West Coast warehouse is chronically out of stock on kitchen appliances while the East Coast warehouse holds surplus. Meanwhile, their operations team has been manually updating inventory spreadsheets when goods are physically shipped between locations — a process that's both error-prone and slow to reflect actual availability on the storefront.
The backend team needs a transfer order module that the ops team can use to initiate, track, and finalize stock movements between warehouses. The module must prevent staff from accidentally over-allocating stock that is already committed to customer orders, and it must keep the available inventory figures accurate throughout the entire transit lifecycle — from the moment a transfer is approved right through to physical receipt at the destination.
Output Specification
Implement the transfer order module as JavaScript. Use in-memory data structures (plain objects/maps) to simulate the database — no real database or external services required. Produce:
- The transfer order management logic as one or more JavaScript modules
transfer-demo.js— a runnable Node.js script (no external dependencies) that:
1. Sets up inventory levels at two locations for a couple of product variants 2. Creates a transfer order moving stock from the over-stocked location to the under-stocked one 3. Calls the ship function and prints inventory state after shipping 4. Calls the receive function with a partial quantity (simulating one item being lost in transit) and prints the final inventory state 5. Demonstrates the validation error when the source location has insufficient available stock
Run the demo script and save its output as transfer-output.txt.
{
"name": "finsi/multi-warehouse",
"version": "0.1.0",
"summary": "Multi-location inventory with allocation rules, transfer orders, and split fulfillment",
"skills": {
"multi-warehouse": {
"path": "SKILL.md"
}
}
}