-
-
Notifications
You must be signed in to change notification settings - Fork 0
Slotting and Replenishment
How openWCS decides where received stock goes and keeps pick faces stocked. This is the
inbound counterpart to Outbound Flow. Authoritative detail:
docs/adr/0003-slotting-and-replenishment.md
and docs/AS-BUILT.md.
The slotting service (port 8093, /api/slotting/**) owns this. It serves two worlds:
A SKU is slotted to a storage block — the whole pool of locations across all aisles — not a
fixed location. The put-away engine chooses the actual location per handling unit at put-away
time, reconciling four competing objectives with one weighted score (per-block weights in
block_policy):
-
Velocity-to-exit — fast movers (class A) near the aisle port, slow movers (C) deep
(
location.distance_to_exit; when it is not maintained, slotting derives a rank-equivalent distance from the cell coordinate as (posX−1)+(posY−1)+(posZ−1), assuming the in/outfeed at position 1, ground level — the guided rack builder also stamps the value explicitly now). -
Same-SKU lane consolidation — fill a partially-used multi-deep lane of the same SKU to avoid
honeycombing / reshuffles (
location.lane_depth). - Aisle redundancy — spread a SKU across aisles so an aisle/crane outage can't strand it and it can be picked in parallel.
- Aisle fill balancing — prefer emptier aisles so travel/throughput stays even.
Hard constraints: lane capacity and a max-%-of-SKU-per-aisle cap (the cap + a min-aisle floor reconcile redundancy with consolidation; the cap is skipped in single-aisle blocks — no alternative aisle exists so the share is always 100%, and enforcing it would reject every SKU's second putaway in a one-aisle block). Single-SKU-per-lane is a soft preference — the lane-affinity term rewards a same-SKU lane and penalises mixing, but a higher balance weight can outweigh it when you want aisle balance to win.
Profile-less SKU fallback: a SKU with no storage profile resolves to the warehouse's only
automated storage block (SHUTTLE_ASRS, CRANE_ASRS, AUTOSTORE, or AMR_GTP, looked up via
GET /api/master-data/storage-blocks?warehouseId=). When several automated blocks exist, slotting
returns 400 — a storage profile must be assigned to disambiguate. This keeps the return-to-storage
leg answerable for demo SKUs and newly received items that haven't been profiled yet.
A SKU+UoM is slotted to a specific location with min/max (pick_slot). Replenishment keeps
it stocked:
- Below-min (reactive): on-hand ≤ min raises a refill-to-max task — EMERGENCY if empty, else SCHEDULED.
- Opportunistic top-off: an off-peak cron refills every face up to max (OPPORTUNISTIC).
- Direct-to-pick: inbound is cross-docked straight to a forward face when it has headroom.
On the off-peak cron, the engine re-runs the put-away scorer over a block's current contents and
recommends moves when a materially better location exists (velocity drift, honeycombing, aisle
imbalance) — gated by block_policy.reslot_shift_pct.
Slotting now turns its PLANS into real device-task moves via flow-orchestrator. A FlowClient
posts to POST /api/flow/moves (see Transport Overview), which dispatches a RELOCATE
(AutoStore: BIN_RELOCATE) to physically relocate the HU; on completion flow books the HU's new
location in inventory and the HandlingUnitMoved txlog audit fires. Three dispatch endpoints:
| Endpoint | What it does |
|---|---|
POST /api/slotting/reslot/{id}/dispatch |
Dispatches a re-slot recommendation as a RELOCATE move (from the recommendation's current → recommended cell). |
POST /api/slotting/replenishment/{taskId}/dispatch |
Resolves a source HU best-effort (FEFO-approx) and dispatches a move from that source to the pick face. |
POST /api/slotting/decant/putaway {warehouseId, huId, skuId, qty} |
Runs the put-away scorer for a freshly decanted HU, then dispatches the STORE to the chosen cell — the goods-in decant → put-away step. |
-
DISPATCHED status: a successfully dispatched plan flips to a new DISPATCHED status, and the
resulting
device_task_idis stamped on the row (migration V7 addsdevice_task_idtoreslot_recommendationandreplenishment_task). - Best-effort: a flow failure leaves the plan un-dispatched (the plan can be retried); the caller distinguishes flow-down from a real rejection.
- Multi-leg: a same-system move is a single RELOCATE leg; a cross-system move (source and destination in different storage blocks) runs a full RETRIEVE → CONVEY → STORE chain in flow-orchestrator, with the STORE leg's device family resolved from the destination block (see Transport Overview).
-
Compose: slotting gained
OPENWCS_FLOW_ORCHESTRATOR_BASE_URLso it can reach flow.
The slotting engine reasons about a multi-deep lane's occupied depth from its own ledger: the active put-away assignments (PLANNED / DISPATCHED) whose chosen cell sits in the lane. Over time that picture drifts from the physical truth: a store/move/pick that completed without its assignment being closed leaves a ghost marking a cell occupied that is actually empty, or a cell gets filled outside the engine.
POST /api/slotting/lanes/reconcile?warehouseId=&blockId?= (RBAC OPERATOR/ADMIN, warehouse-scoped;
a single block when blockId is given, else all blocks) reconciles the ledger against reality. For
every multi-deep lane (lane_depth > 1, cells grouped by aisle + vertical level + column) it:
- reads the engine's ledger (active assignments per cell);
- asks inventory which of those cells are actually occupied, reusing the
POST /api/inventory/locations/occupiedprojection; - reports per-lane drift as
{blockId, laneKey, locationId (face cell), expectedDepth, actualDepth}; - corrects ghosts: where the ledger holds an assignment for a cell inventory reports empty, the assignment is closed (status RECONCILED) so the engine stops counting it occupied.
An off-peak ShedLock sweep (LaneReconciliationScheduler, cron openwcs.slotting.offpeak-cron,
default 02:00) runs the same reconciliation across every warehouse. It is best-effort: if
inventory occupancy is unavailable the lane is skipped and the pass degrades to an empty report rather
than crashing.
- Slotting is configured in the UI ("Slotting" tab): assign pick faces (SKU+UoM, min/max, direct-to-pick) and block slotting (SKU → block, velocity class, redundancy knobs).
- Velocity/ABC class is self-taught: the service consumes
txlog.streammovements and keeps a per-SKU recency-weighted (EWMA) pick-frequency, then an off-peak job assigns A/B/C — unless amanual_overridepins it. So new/seasonal SKUs ramp to A fast and decay. -
Risers/fallers (dashboard): the velocity consumer also upserts one row per SKU per day into
sku_pick_daily(V6 migration, idempotent). The ABC Movers dashboard reads this table to compute exact trailing-14d-vs-90d windowed pick rates and surfaces the biggest risers and fallers — a more precise signal than the EWMA short-vs-long proxy that preceded it. The EWMA still drives the main A/B/C class assignment. - Key endpoints:
POST /api/slotting/putaway,POST /api/slotting/putaway/stored(store confirmation — see assignment lifecycle below),POST /api/slotting/replenishment/{plan,top-off},GET …/replenishment/tasks,POST /api/slotting/reslot/recommend,POST /api/slotting/relocation-plan(dig-out plan for a blocked multi-deep retrieve — see below), plus CRUD for storage profiles / pick slots / block policies and master-datastorage-blocks. -
Assignment lifecycle: a fresh
POST /api/slotting/putawaycall supersedes any open (PLANNED/DISPATCHED) assignments for the same HU first — a tote can only travel to one slot at a time, so each workplace round-trip re-ask leaves the ledger bounded at one live plan per tote. Once the ASRS STORE completes, flow-orchestrator callsPOST /api/slotting/putaway/stored {warehouseId, huId}(best-effort, like the HU location booking); the assignment moves to STORED status and the cell appears as physically occupied via the inventory check rather than as planned occupancy. Response:{"closed": N}. -
Block-policy scorer-weight field names (PUT/GET
/api/slotting/block-policies/{blockId}):wVelocity,wConsolidation,wRedundancy,wBalance— all lowercase-wcamelCase. Java's bean-naming convention would otherwise capitalise the leading pair (WVelocity), so@JsonPropertypins the lowercase contract; use exactly these names in API calls and the UI — mismatched capitalisation silently leaves weights at their default of1. - A goods-in BPMN
assignPutawaydelegate calls the engine and sets the target location for the downstream move. -
HTTP error codes:
400— invalid input (missing or malformed fields);404— referenced entity not found;409— infeasible placement (e.g. no feasible storage location in block, no storage profile for SKU and multiple automated blocks exist). Callers that treat slotting as best-effort (e.g. flow-orchestrator's return-to-storage leg) should distinguish 409 from 5xx: 409 means the request was understood but physically unsatisfiable and will not succeed on retry; 5xx means a transient fault (flow leaves the tote circulating on the conveyor and retries viaInductionSlotSweeper). Prior to f1b9603, infeasible placements returned 500.
Every material decision in the slotting service is logged at INFO (or WARN for degraded
paths) so an engineer can trace a fault from the daily log alone without enabling DEBUG.
API rejections (logged by ApiExceptionHandler before the HTTP response is sent — useful when
the caller is best-effort and only surfaces a one-line warning on its own side):
| Status | Reason | Level | Log message pattern |
|---|---|---|---|
| 400 | Invalid input | WARN | request rejected (400): <message> |
| 404 | Entity not found | WARN | request rejected (404): <message> |
| 409 | Infeasible placement | WARN | request rejected (409): <message> |
Put-away (logged immediately before the PutawayDecision is returned):
| Decision path | Log fields |
|---|---|
| Scored block placement |
hu, dominant SKU, chosen location, block, score, factor map |
| Direct-to-pick |
hu, SKU, pick-face location, headroom |
| Empty-HU placement |
hu, location, block, distance-to-exit, LOW priority
|
| Open assignment superseded on re-assign | assignment id, HU id, previous chosen location |
| Store confirmation received (STORED) | assignment id, HU id, confirmed storage location |
Replenishment:
| Event | Level | Key fields |
|---|---|---|
| Task created | INFO | pick-face location, SKU, on-hand, min/max, refill qty, trigger type, priority |
| Dedup skip — open task exists for this face (below-min) | WARN | pick-face, SKU, on-hand, min threshold |
| Dedup skip — open task exists for this face (top-off) | DEBUG | pick-face |
Re-slotting:
| Event | Level | Key fields |
|---|---|---|
| Move recommended | INFO | HU, SKU, from/to location, block, score gain vs shift threshold |
| Per-run cap reached (200 recommendations) | WARN | block, cap limit, consequence (remaining HUs re-evaluated next off-peak pass) |
Relocation planning (dig-out):
| Event | Level | Key fields |
|---|---|---|
| Plan produced | INFO | source location, blocker count, each move as hu <code> <from> -> <to>
|
| Unplannable — no free same-level target | WARN | source location, blocker cell Y, consequence (retrieve stays blocked) |
Velocity / ABC reclassification:
| Event | Level | Key fields |
|---|---|---|
| Class updated | INFO | SKU, old class → new class, block, decayed EWMA score |
| Manual-override skip | DEBUG | SKU, pinned class, block, learned class |
Block-policy save:
| Event | Level | Key fields |
|---|---|---|
| Policy created or updated | INFO | block, warehouse, all four scorer weights, reslot enabled/disabled, shift threshold, off-peak cron |
-
HU type capabilities (master-data → Handling unit types admin catalog):
compartments(1–8),storable_in_automation,transportable_on_conveyor, plus L×W×H dimensions (mm), weight limit (g), and nestable flag. Full CRUD viaMaster Data → Handling unit typesin the UI. Each storage block also has anallowed_hu_typeslist, so an automated area only accepts the HU types it permits (e.g. conveyable but not storable). In the UI this is configured as a chip multi-select of active (non-archived) HU type names — no free text — in both the standard block dialog and the new Guided builder (which also bulk-generates rack locations from aisle × levels × positions × sides). - Multi-compartment HUs (1–8 SKUs): the dominant compartment (most qty) drives velocity placement; lane affinity matches on the compartment SKU set (a single-SKU HU is a one-element set).
- Empty HUs (no SKU) are stored farthest from the exit and moved at LOW transport priority — a buffer for decanting.
-
Cell-level locations: a storage location row is one cell, uniquely identified by
(warehouse, aisle, side, x, y, z), so a handling unit's position is always exactly known; a multi-deep lane = the cells sharing(aisle, side, x, y)overz(z1 = face … zN = deepest). Conveyors/stations are locations too (a function group), so an HU in transit is located at its conveyor. -
Live-occupancy filter: before scoring, the engine asks inventory which of the block's
candidate locations physically hold any stock row or handling unit
(
POST /api/inventory/locations/occupied) and drops those, so a seeded/occupied slot with no slotting assignment is never chosen. The call is best-effort — if inventory is unreachable it is logged and skipped so put-away still proceeds.
For a multi-deep lane, a retrieve at cell Z N is physically blocked by any HU occupying a lower Z
in the same channel (aisle + side + posX + posY). POST /api/slotting/relocation-plan
(ADR 0009 step 1) returns the ordered sequence of single-shuttle relocations the WCS must execute
before the retrieve can proceed.
Request: { "warehouseId": "…", "locationId": "…" } — locationId is the retrieve's source
slot (the cell containing the target HU).
Response:
{
"steps": [
{ "huId": "…", "huCode": "TTE-001", "fromLocationId": "…", "toLocationId": "…" }
],
"blocked": false
}-
stepsare ordered front-most (lowestposZ) first — the shuttle must clear the aisle face before it can reach deeper cells. -
Target selection is subject to a hard same-level constraint: the target must share
posYwith the blocker (the shuttle serves one level; changing level requires an expensive lift move). Within that constraint, targets are ranked: same aisle → same side → shallow (posZ 1) → closest along the aisle (|ΔposX|) → location code (deterministic tiebreak). The channel being cleared is never a valid target; no target is reused within one plan. -
blocked: true,steps: []— a blocker exists but no valid same-level free slot was found; the caller should halt the retrieve and escalate rather than attempt it. -
blocked: false,steps: []— the channel is already clear; the retrieve can go immediately.
Occupancy is read from the live HU registry (GET /api/inventory/handling-units); cell coordinates
from master-data. If the registry is unreachable the planner returns clear (fail-open) rather than
acting on stale data.
Status (ADR 0009): planning endpoint ✅ (step 1); physical dig-out dispatch ✅ (step 2) — flow-orchestrator executes the RELOCATE/BIN_RELOCATE chain before every blocked retrieve (see Equipment Integration); stored-tote rack view ✅ (step 3 — see Hardware Visualisation).
Status: the engine computes plans, exposes them, and now dispatches them as physical moves (re-slot / replenishment / decant put-away → flow
POST /api/flow/moves; plans flip to DISPATCHED; V7 stampsdevice_task_id). The physical-move audit foundation is in — flow-orchestrator appends aHandlingUnitMovedevent to the txlog system-of-record on every completed/failed STORE/RETRIEVE/RELOCATE it executes (see Transport Overview). The multi-leg RETRIEVE → CONVEY → STORE move chain is now built (with destination-block-resolved STORE family), and lane-depth ledger↔inventory reconciliation is now built (POST /api/slotting/lanes/reconcile+ an off-peak sweep, above). Still tracked fast-follows: HU on-conveyor booking and true (not approx) FEFO replenishment sourcing. See Roadmap and Status · Goods-to-Person Stations.
See Services · Architecture · Equipment Integration.
openWCS — open-source Warehouse Control System · summarized from build.md & docs/AS-BUILT.md (the repo docs are authoritative).
Design
Flows
- Inbound and Inventory
- Slotting and Replenishment
- Goods-to-Person Stations
- Outbound Flow
- Equipment Integration
- Transport Overview
- Process Designer
- Mobile Process Designer
- Hardware Visualisation
- Host Integration
Reporting & Dashboards
Operations