-
-
Notifications
You must be signed in to change notification settings - Fork 0
Goods to Person Stations
How openWCS runs a goods-to-person (GTP) workstation: the operator stays put while goods are
brought to them, picks stock from a presented stock handling unit (HU), and puts it into the
correct order destinations under put-to-light. Authoritative detail:
docs/adr/0006-gtp-station-execution.md
and docs/AS-BUILT.md.
The gtp service (port 8094, /api/gtp/**) owns station configuration and the pick-and-put
work cycle. It is downstream of Outbound Flow (allocation produces the demand) and is fed by
Equipment Integration (ASRS/AMR/conveyor bring the HUs; lights are device adapters).
A station has a mode and a set of nodes:
- STOCK node (≥1) — where a stock HU (one SKU) is presented to the operator (pick-to-light shows how many to take).
- ORDER node (1..N) — an order destination holding an order HU, with an optional put-light.
Two physical realisations, one logical shape:
- ORDER_LOCATION — order HUs in fixed locations, usually a conveyor spur (HU-in-location). Most non-AMR systems / two-sided put-walls.
- PUT_WALL — a rack of lit cubbies, each holding an order HU. Most AMR goods-to-rack systems.
-
Configure the station + nodes; open an order destination on an ORDER node by binding an
order HU and posting its demand (a SKU + the qty to put there).
POST /api/gtp/stations·POST /api/gtp/stations/{id}/nodes·POST /api/gtp/stations/nodes/{nodeId}/destinations. -
Present a stock HU → the service matches the SKU against open demand across the station's
ORDER nodes and greedily fills most-needed-first, returning a put-list (destination +
put-light + qty). One stock HU serves many destinations — the goods-to-person batch.
POST /api/gtp/stations/{id}/present. -
Confirm each put (optionally a short qty) → decrements the cycle's remaining stock and the
destination's demand; a fully-putted destination completes.
POST /api/gtp/puts/{id}/confirm. -
Query / close:
GET /api/gtp/cycles/{id}·GET /api/gtp/stations/{id}/demand·POST /api/gtp/cycles/{id}/close.
Both topologies use the same engine — mode only documents what an ORDER node represents
physically (a conveyor order-HU location vs. a lit rack cubby).
mode (ORDER_LOCATION vs PUT_WALL) is the destination topology. Orthogonal to it, a station
supports a set of operating modes = what the operator does with a presented HU. Each work
cycle runs one operating mode and carries the mode-appropriate task lines (the put-list
generalised), each with an outcome/confirmation.
| Operating mode | Present | Task lines | Outcome on confirm | Seam |
|---|---|---|---|---|
| PICKING | a stock HU (1 SKU) | put-instructions (the put-list) | put / short put | — (the existing flow, unchanged) |
| DECANTING | a source HU + an empty target HU | decant-moves (SKU + qty → target compartment) | moved qty | filled target + SKUs → slotting put-away |
| DECANT_MULTI | a source HU + several empty target HUs | decant-moves spread across multiple targets | moved qty per target | filled targets + SKUs → slotting put-away |
| STOCK_COUNT | an HU | count entries (SKU + expected qty) | counted qty; variance = counted − expected | non-zero variance → inventory StockAdjusted |
| QC | an HU | verdict slots per HU/SKU |
PASS / FAIL / HOLD (quarantine) |
— |
| MAINTENANCE | HUs / empty carriers | check slots |
OK / DEFECTIVE / REPAIR
|
— |
REST (additive; the PICKING endpoints below are unchanged):
-
POST /api/gtp/stations/{id}/operating-modes {supportedModes[]}— configure supported modes (PICKING is always retained). -
POST /api/gtp/stations/{id}/cycles {operatingMode, stockHuId?, targetHuId?, skuId?, qty?, lines[]}— open a cycle in any mode and present its HU(s). PICKING delegates topresent. -
POST /api/gtp/tasks/{taskLineId}/outcome {actualQty?, verdict?}— submit a per-line outcome.
The two mode seams (DECANTING → slotting put-away, STOCK_COUNT → inventory StockAdjusted) are
documented seams, not hard-wired: gtp exposes the filled target HU + its SKUs, and the
non-zero count variances, for a caller/adapter to act on — it does not call slotting or adjust
inventory itself.
A GTP station is also a physical place in the warehouse. In the Automation Topology editor a
station appears in the GTP workplaces library group and is placed as "workstation" equipment
(a green box referencing the gtp_station). Once placed, the Properties panel's Conveyor
interactions editor links the workstation to the specific conveyor function points that feed
it, each tagged with a role — STOCK (stock-feed conveyor), ORDER (order conveyor/destination),
or DECANT. This models how stock and order HUs physically reach the operator. See
Equipment Integration.
In the GTP workplace config screen, the destination topology (ORDER_LOCATION vs
PUT_WALL) is only requested when PICKING is one of the workplace's supported operating modes
— it only describes where order destinations sit, which is meaningless for non-picking modes. Node
configuration uses a location-code lookup (search by location code, not a raw UUID).
The induction queue is owned by flow-orchestrator (ADR-0007 Phase 3c-1, implemented 2026-06). Callers submit a single request to flow; flow orchestrates the full RETRIEVE + CONVEY journey and drives the entry through a four-state lifecycle:
| Status | Meaning |
|---|---|
| REQUESTED | Presentation request accepted; tote is in the backlog. Flow meters retrievals into the cap — REQUESTED entries are uncapped. Visible on the workplace screen before the tote is physically in transit. |
| IN_TRANSIT | RETRIEVE device task completed; CONVEY leg dispatched. Tote is on its way to the station. |
| QUEUED | CONVEY arrival callback received; tote is at the station and ready to work. Assigned an arrivalSeq (arrival order, not request order). |
| DONE | Operator completed this entry. Frees a cap slot and re-meters the REQUESTED backlog. Flow immediately dispatches the return-to-storage leg (see Return-to-storage leg). |
Only {IN_TRANSIT, QUEUED} count against the workplace's in-transit cap (maxInTransitPicking /
maxInTransitOther read from the GTP station config, best-effort); REQUESTED is an uncapped backlog.
Flow endpoints (authoritative source of truth):
-
POST /api/flow/induction/requests {warehouseId, workplaceId, workplaceKind, huId, huCode, skuId, skuCode, qty, locationId, mode, family, countTaskId?, countLineId?}— request HU presentation. Creates aREQUESTEDentry and triggers the retrieval pipeline. Returns the new entry. -
GET /api/flow/induction/queue?workplaceId=— full{REQUESTED, IN_TRANSIT, QUEUED}pipeline in arrival order (DONE excluded). This is the queue the operator console reads. -
POST /api/flow/induction/entries/{id}/done— mark an entry DONE (operator-driven). Idempotent. -
GET /api/flow/induction/entries/{id}— single entry read.
Legacy GTP queue endpoints (POST/GET /api/gtp/stations/{id}/queue, POST /api/gtp/queue/{id}/complete) remain in place but are deprecated — counting no longer calls them and new callers should use the flow endpoints above.
When an entry is marked DONE via POST /api/flow/induction/entries/{id}/done, flow-orchestrator owns the full return journey — only slotting decides where the tote goes; the source slot is never used as a fallback:
-
Slotting asked — flow calls
POST /api/slotting/putawayfor the destination. The entry'sstorageLocationIdis stamped when slotting answers. -
Return CONVEY dispatched — a
CONVEYdevice task (familyCONVEYOR) carries the tote from the station. The entry'sreturnConveyTaskIdis set; trace eventRETURNINGis written.-
Slotting answered → the CONVEY is routed to the storage entry node;
storageLocationIdis set on the entry. -
Slotting errored / no answer → the CONVEY goes out with no destination and no route plan (the tote must leave the workplace); the entry is flagged
awaitingSlot = true. A ShedLock-guarded sweep (InductionSlotSweeper, ~30 s, backed byV16__shedlock.sql) retries slotting until it answers — then stampsstorageLocationId, assigns the destination + route plan mid-journey (per-scan routing adapts the walking tote;SLOT_ASSIGNEDtrace row), and clears the flag. If the plan-less CONVEY already completed, the STORE is fired directly instead.
-
Slotting answered → the CONVEY is routed to the storage entry node;
-
Return CONVEY completes — flow receives the callback, writes trace event
RETURN_ARRIVED, and dispatches the store task (oncestorageLocationIdis known). -
STORE / BIN_STORE dispatched — family and command resolved from the entry's family field (
AUTOSTORE→BIN_STORE; all others →STORE); payload carries the slotting-chosenstorageLocationId. The entry'sreturnStoreTaskIdis set. -
STORE completes — trace event
STOREDatslot:<storageLocationId>closes the HU's timeline.
HU location bookings: every CONVEY dispatch books the HU to the entry conveyor's operational location (GET /api/master-data/locations/operational?kind=EQUIPMENT&name=<conveyor>); QUEUED books the workplace's operational location (kind=WORKPLACE); STORE completion books the final slot. Unresolvable → null (inventory maps it to UNKNOWN); bookings are always best-effort (WARN on failure).
The return CONVEY is dispatched exactly once (returnConveyTaskId guards against double-dispatch; a repeat markDone call on an already-DONE entry is a no-op). returnConveyTaskId, returnStoreTaskId, storageLocationId, and awaitingSlot are all returned on InductionEntryView and visible in the Transport Overview trace dialog.
Schema: V12 (V12__induction_return_leg.sql) adds return_convey_task_id / return_store_task_id; V15 (V15__induction_slotting_storeback.sql) adds storage_location_id + awaiting_slot; V16 (V16__shedlock.sql) adds the flow.shedlock table used by InductionSlotSweeper.
Dirty-tote exception: a tote flagged via POST /api/gtp/stations/{id}/exceptions/dirty-tote is marked DONE without triggering the return leg — it goes to a CLEANING maintenance order instead (see Station exceptions).
acceptingWork controls whether a station takes new HUs:
-
POST /api/gtp/stations/{id}/deactivate— setsacceptingWork=false. The station finishes its already-queued work but rejects new inbound HUs (409). Useful for shift changes and maintenance windows. -
POST /api/gtp/stations/{id}/activate— restoresacceptingWork=true.
The WorkplaceView response (returned by the session and workplace APIs) includes acceptingWork, so the operator console can seed the drain-toggle state on page reload without an extra request.
maxInTransitPicking and maxInTransitOther (defaults: 4 / 2) control how many HUs may have an
active inbound transport to the station simultaneously, split by mode class (PICKING vs all others).
-
POST /api/gtp/stations/{id}/capacity {maxInTransitPicking, maxInTransitOther}— replaces both caps. Both values must be ≥ 0.
Schema: V5__station_in_transit_caps.sql (adds the cap columns + accepting_work to gtp_station); V6__station_queue.sql (creates station_queue_entry).
POST /api/gtp/stations/{id}/nodes/sync {nodes[{role, code, locationId, putLightId, inboundDistanceM}]} — replaces the station's STOCK/ORDER nodes from a topology-projected node set. Nodes are matched by code: an existing node keeps its id and any bound order HU / demand; nodes absent from the projection are removed only when they carry no open demand. The inboundDistanceM value (metres of feeding conveyor, taken from the topology function-point's offsetM) is stored on the node (station_node.inbound_distance_m, V7__station_node_distance.sql) and is the source for the queue-timing fallback above.
This endpoint is called automatically by flow-orchestrator as a best-effort side-effect of every topology projection (POST /api/flow/automation/topology/project): the routing graph is persisted first, then each GTP workstation's STOCK/ORDER conveyor interactions (set in the topology editor's Properties panel) are projected into the bound station's nodes. The GTP HTTP call is bounded by a 2-second connect/read timeout; a slow or unreachable GTP service cannot stall "Generate routing". Any failure is swallowed and logged as a warning.
Two operator-raised exceptions are available at a GTP station while a tote is present at the head of the queue. They are accessible from the Exceptions drawer in the operator console (a second right-edge fold-out, stacked below the Queue drawer handle).
| Exception | Endpoint | Behaviour |
|---|---|---|
| Dirty tote | POST /api/gtp/stations/{id}/exceptions/dirty-tote {queueEntryId} |
Opens a CLEANING MaintenanceOrder for the tote and marks the queue entry DONE without a store-back. The tote goes to maintenance rather than returning to storage. Returns the maintenance order. |
| Broken product | POST /api/gtp/stations/{id}/exceptions/broken {queueEntryId, qty} |
Posts a negative StockAdjusted event (reason DAMAGED) to the txlog for qty units of the tote's SKU; inventory writes off the loss. The tote stays in the queue — the operator keeps working it. qty must be greater than 0. The actor is taken from the X-Auth-User header (falls back to "system"). Returns { "adjusted": <qty> }. |
Count tasks for automated-storage cells are routed asynchronously: task creation returns
immediately with routingStatus = PENDING and the background CountRoutingScheduler handles
routing within a minute.
When the routing sweep runs for a PENDING (or FAILED) task with the hardware emulator ON:
- For each unrouted
CountLine, the counting service resolves the cell's storage block type via master-data. - If the block is an ASRS-family type (
SHUTTLE_ASRS,CRANE_ASRS,AUTOSTORE,AMR_GTP) and the line is not yet markedrouted, it looks up the tote (HU) from inventory. - It issues a single
POST /api/flow/induction/requests(ADR-0007 Phase 3c-1): flow creates aREQUESTEDentry and orchestrates the full RETRIEVE + CONVEY journey itself. The capacity gate lives in flow —REQUESTEDis uncapped; only{IN_TRANSIT, QUEUED}count against the cap — so aFAILEDresponse means the call itself failed (e.g. flow unreachable), not a capacity rejection. The line is markedrouted = trueon success (committed immediately, not rolled back if a later line fails — prevents duplicate induction requests on retry).
After all lines are processed the task's routingStatus and routingReason are persisted:
| Status | Meaning |
|---|---|
| PENDING | Not yet attempted (initial state). |
| ROUTED | All automated-storage lines successfully routed to a counting station. |
| NOT_REQUIRED | No automated-storage cells, or none had a handling unit to move. |
| FAILED | Emulator off, no active STOCK_COUNT station, station in-transit cap reached (enqueue rejected), or a transport/enqueue call failed. Retried automatically every minute once a slot frees up. |
The scheduler (CountRoutingScheduler, ShedLock-protected so only one replica sweeps at a time)
retries all OPEN tasks whose status is PENDING or FAILED. CountRoutingService.routeTask is
idempotent — already-routed lines are skipped, so re-running never duplicates a transport.
Both routingStatus and routingReason are returned on CountTaskView and shown in the Counting
screen as a colour-coded badge (green ROUTED / red FAILED / yellow PENDING / plain "n/a"
NOT_REQUIRED) with routingReason as a tooltip.
The GTP workplace config screen exposes capacity configuration (in-transit HU caps per mode class). See Operator console below for how the console surfaces the queue and drives operator actions.
The GTP operator console (/gtp-ops/:stationId) is queue-driven: it polls the station queue
every 3 s and drives the operator workflow automatically — no manual data-entry form.
Operating mode is persisted per station in localStorage (key openwcs.gtp.mode.<stationId>):
the operator's last-chosen mode is restored automatically on page reload, so they don't need to
re-select it after a refresh or browser restart.
Per-mode accent hue gives the operator an at-a-glance colour cue for the active mode: PICKING
and DECANTING use the default green (--herbal-lime), STOCK_COUNT uses orange (--warning),
and QC / MAINTENANCE use red (--danger). The accent is applied to the mode-selector pills and
to the glass-panel borders of the active-tote panel, CountPanel, and mode-placeholder panel
(modeAccent() in GtpOpsScreen.tsx).
When a QUEUED tote is at the station the console shows an active-tote panel with:
- Tote glyph + HU code: a small open-top tote line icon (in the mode accent colour) left of the HU code hero line identifies the physical tote; in PICKING the quantity sits beside it.
- SKU code: prominent, with the SKU name/description on its own line underneath.
-
Metadata chips: a subtle strip under the name: item dimensions (
90 x 280 x 95 mm) and weight (850 g/1.4 kg) from the SKU's base unit of measure, then every scalarsku_profilemetadata entry (Brand, Color, Season, ... whatever keys exist) as a key-capitalised chip. Nothing to show means no chips, no empty chrome. - SKU image — displayed when available in master data.
The details come from one lightweight master-data read,
GET /api/master-data/skus/{skuId}/card?warehouseId= (identity + base-UoM dimensions/weight +
the warehouse profile's metadata), cached per (SKU, warehouse) for the session. The panel never
waits for it: identity renders straight from the queue entry; chips fill in when the card arrives.
The same identity block renders in the blind-count CountPanel.
When no work cycle is active (head tote is QUEUED but not yet presented, or in STOCK_COUNT mode) the panel fills the viewport: the SKU image enlarges responsively and the layout is centred, giving the operator a clear visual of what is next before they begin the cycle.
- The head QUEUED entry is auto-presented: the console calls
POST /api/gtp/stations/{id}/presentin the background, starting a pick cycle without operator input. - The put-to-light task list appears beneath the active-tote panel; the operator confirms each put.
- When the last put is confirmed, the work cycle closes, the queue entry is marked DONE
(
POST /api/gtp/queue/{entryId}/complete), and the next head tote is auto-presented. - If
presentfails (e.g. no open demand for the SKU), an inline error is shown and a Mark tote done & advance button lets the operator skip the tote.
Also queue-driven with the same active-tote panel (full-screen when no cycle is active).
When the queue entry carries countTaskId/countLineId (set by the counting service for
GTP-routed count totes) the console switches to a dedicated CountPanel (large SKU image,
prominent HU/SKU labels, oversized numeric input — no browser spinner arrows):
- Operator enters the quantity they physically count on the tote — no system qty is shown (blind).
- The console calls
POST /api/counting/tasks/{countTaskId}/lines/{countLineId}/station-count. - The response
outcomedrives the next step:- ACCEPTED — count matched; the panel advances and the queue entry is marked DONE.
- RECOUNT — first count didn't match the system qty; the panel clears and the operator counts again.
-
ADJUSTED — two counts agreed and differed from the system qty; a
StockAdjustedevent (reasonCOUNTING) was posted to the host; the panel advances.
If the queue entry has no countTaskId/countLineId (manually enqueued tote) a plain
Done counting button marks the entry DONE and advances to the next tote.
When the head QUEUED tote requires a different operating mode than the station is currently in
(e.g. a STOCK_COUNT tote arrives while the console is in PICKING), the work area is replaced with
a mode-mismatch panel rather than showing a blank "nothing to do" state or presenting the tote
under the wrong flow.
The panel names both the required and the current mode, then offers:
- Switch to [mode] button — if the workplace's supported modes include the required mode, a single tap changes the active mode immediately.
- Routing instruction only — if the workplace is not configured for the required mode, the panel explains that the tote must be routed to a compatible station instead.
The PICKING auto-present effect is guarded by the same check: a tote whose mode field is not
PICKING is never auto-presented, so a mis-routed count tote cannot be accidentally presented as a
pick.
The mismatch panel is suppressed while a pick cycle is in progress — an in-flight cycle is never interrupted by a mode switch.
The inbound queue is surfaced as a right-side fold-out drawer — a slim vertical handle on the
right edge shows the inbound count; clicking it slides the panel open over a backdrop. The drawer
lists all REQUESTED, IN_TRANSIT, and QUEUED entries in arrival order (the full pipeline view),
with the head entry highlighted and a live ETA countdown for IN_TRANSIT entries. REQUESTED entries
show the tote that is still in the backlog waiting for a retrieval slot. The drawer is read-only;
operator actions happen in the main console area.
A second right-edge fold-out labelled Exceptions (stacked below the Queue handle) is available in every work mode while a session is live. It surfaces two operator actions on the current head tote:
-
Mark tote as dirty — sends the tote to maintenance and advances to the next tote (calls
POST /api/gtp/stations/{id}/exceptions/dirty-tote; a CLEANING maintenance order is opened and the entry completes without a store-back). -
Mark product as broken — reveals a numeric input for the damaged qty; confirming posts a DAMAGED stock adjustment and leaves the tote in place (calls
POST /api/gtp/stations/{id}/exceptions/broken).
Both show inline success/error feedback. The actions are disabled when no tote is at the station.
When the queue is empty and no cycle is active the console shows a full-screen Waiting for totes panel (large icon, centred, fills the remaining viewport height). Deactivate / activate controls remain accessible in the console footer.
If the presented qty can't cover all demand, the surplus demand stays OPEN for the next stock
HU of that SKU. A short confirm (fewer units than lit) closes the instruction SHORT and
leaves the destination's remaining demand open.
Every significant decision in the gtp service is logged at INFO (state changes, allocations,
session events) or WARN (rejected/skipped paths) so that a full station session is traceable
from production logs without DEBUG enabled:
| Path | Level | Key fields |
|---|---|---|
| Work cycle present: put-list built | INFO | station code, put count, allocated vs presented qty, stock HU, SKU, cycle id |
| Work cycle present: no open demand | INFO | SKU, station code, qty, stock HU, cycle id |
| Cycle started (any mode) | INFO | mode, cycle id, station code, task-line count, stock HU |
| Put confirm | INFO | put id, station code, confirmed qty, order ref, SKU, remaining demand |
| Put confirm SHORT | WARN | put id, station code, confirmed vs lit qty, order ref, SKU, remaining demand |
| Destination demand completed | INFO | order ref, SKU, station code |
| Work cycle completed | INFO | mode, cycle id |
| Work cycle closed early | WARN | mode, cycle id, stock HU, cancelled puts, cancelled task lines |
| Topology node sync | INFO | station code, matched/added/removed/kept-with-demand counts |
| Stale node kept (open demand) | WARN | station code, stale node code |
| Destination opened | INFO | node code, order ref, qty, SKU |
| Station status change | INFO | station code, old → new status |
| Supported modes set | INFO | station code, new mode list |
| In-transit caps set | INFO | station code, picking cap, other cap |
| Station activate/drain | INFO | station code, accepting or draining |
| Enqueue rejected (status/mode/cap) | WARN | station code, reason, in-flight count, HU code, SKU |
| Induction entry completed | INFO | entry id, workplace id, tote HU, SKU, mode |
| Workplace session claimed | INFO | workplace code, operator, session id |
| Session taken over | INFO | workplace code, superseded session id, old/new operator |
| Session released | INFO | session id, operator, station id |
-
Physical hardware — the actual put-lights and retrieving picking-HUs to the station via
ASRS/AMR/conveyor — are device-adapter + flow-orchestrator concerns.
gtprecordsputLightId/stockHuId/orderHuIdand exposes the instructions. Count-tote retrieval (for all automated storage families — ASRS, AutoStore, AMR) is wired (emulator mode only, via the counting service). Store-back is fully owned by flow-orchestrator (slotting-only; no GTP-side STORE). - Demand origination — posted over REST today; a follow-up auto-wires it from allocation pick batches / order lines.
- Stock decrement → txlog — confirmations are cycle-local; a follow-up appends the moves to the transaction log like the other services.
Schema gtp: gtp_station (+ supported_modes + accepting_work + max_in_transit_picking / max_in_transit_other), station_node (+ inbound_distance_m), destination_demand, work_cycle (+ operating_mode, target_hu_id), put_instruction, task_line, station_queue_entry (legacy deprecated — V6__station_queue.sql; + count_task_id, count_line_id — V8; + location_id — V9), maintenance_order (V9; columns: warehouse_id, hu_id, hu_code, gtp_station_id, sku_id, sku_code, reason (CLEANING), status (OPEN/CLOSED)).
Schema flow (ADR-0007 Phase 3c-1, V11–V16): induction_queue_entry (the live queue — REQUESTED → IN_TRANSIT → QUEUED → DONE; V12 adds return_convey_task_id / return_store_task_id; V15 adds storage_location_id + awaiting_slot), hu_transport_trace (append-only HU timeline; return-leg events RETURNING, RETURN_ARRIVED, STORED, SLOT_ASSIGNED added by V12/V15 flow logic), shedlock (V16 — cluster-wide lock table for InductionSlotSweeper).
Orders / SKUs / HUs / locations are referenced by UUID (no cross-schema FKs).
When the single SKU per compartment stock rule is ON (master-data GET /api/master-data/stock-rules, default ON, admin-toggleable from Settings → Stock rules), starting a DECANTING cycle is rejected when a compartment would receive two different SKUs, or when the cycle carries more distinct SKUs than the target tote's HU type has compartments (HU type resolved via the inventory HU registry). Switch the rule off to allow deliberate mixing.
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