Skip to content

Goods to Person Stations

theGreenGuy edited this page Jun 12, 2026 · 24 revisions

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).

Station anatomy

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.

The pick-and-put work cycle

  1. 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.
  2. 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.
  3. 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.
  4. Query / close: GET /api/gtp/cycles/{id} · GET /api/gtp/stations/{id}/demand · POST /api/gtp/cycles/{id}/close.

Both topologies use the same enginemode only documents what an ORDER node represents physically (a conveyor order-HU location vs. a lit rack cubby).

Operating modes

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 to present.
  • 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.

Placing a workplace in the layout

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 roleSTOCK (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).

Station inbound queue

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 a REQUESTED entry 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.

Return-to-storage leg

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:

  1. Slotting asked — flow calls POST /api/slotting/putaway for the destination. The entry's storageLocationId is stamped when slotting answers.
  2. Return CONVEY dispatched — a CONVEY device task (family CONVEYOR) carries the tote from the station. The entry's returnConveyTaskId is set; trace event RETURNING is written.
    • Slotting answered → the CONVEY is routed to the storage entry node; storageLocationId is 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 by V16__shedlock.sql) retries slotting until it answers — then stamps storageLocationId, assigns the destination + route plan mid-journey (per-scan routing adapts the walking tote; SLOT_ASSIGNED trace row), and clears the flag. If the plan-less CONVEY already completed, the STORE is fired directly instead.
  3. Return CONVEY completes — flow receives the callback, writes trace event RETURN_ARRIVED, and dispatches the store task (once storageLocationId is known).
  4. STORE / BIN_STORE dispatched — family and command resolved from the entry's family field (AUTOSTOREBIN_STORE; all others → STORE); payload carries the slotting-chosen storageLocationId. The entry's returnStoreTaskId is set.
  5. STORE completes — trace event STORED at slot:<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).

Deactivate / drain control

acceptingWork controls whether a station takes new HUs:

  • POST /api/gtp/stations/{id}/deactivate — sets acceptingWork=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 — restores acceptingWork=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.

In-transit capacity caps

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).

Topology node sync

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.

Station exceptions

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-tote routing (counting → GTP seam)

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:

  1. For each unrouted CountLine, the counting service resolves the cell's storage block type via master-data.
  2. If the block is an ASRS-family type (SHUTTLE_ASRS, CRANE_ASRS, AUTOSTORE, AMR_GTP) and the line is not yet marked routed, it looks up the tote (HU) from inventory.
  3. It issues a single POST /api/flow/induction/requests (ADR-0007 Phase 3c-1): flow creates a REQUESTED entry and orchestrates the full RETRIEVE + CONVEY journey itself. The capacity gate lives in flow — REQUESTED is uncapped; only {IN_TRANSIT, QUEUED} count against the cap — so a FAILED response means the call itself failed (e.g. flow unreachable), not a capacity rejection. The line is marked routed = true on 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.

Operator console

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).

Active-tote panel

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 scalar sku_profile metadata 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.

PICKING mode

  • The head QUEUED entry is auto-presented: the console calls POST /api/gtp/stations/{id}/present in 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 present fails (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.

STOCK_COUNT mode

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):

  1. Operator enters the quantity they physically count on the tote — no system qty is shown (blind).
  2. The console calls POST /api/counting/tasks/{countTaskId}/lines/{countLineId}/station-count.
  3. The response outcome drives 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 StockAdjusted event (reason COUNTING) 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.

Mode-mismatch panel

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.

Queue drawer

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.

Exceptions drawer

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.

Idle state

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.

Short-pick / exception handling

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.

Observability

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

Seams (fast-follow, not built)

  • Physical hardware — the actual put-lights and retrieving picking-HUs to the station via ASRS/AMR/conveyor — are device-adapter + flow-orchestrator concerns. gtp records putLightId/stockHuId/orderHuId and 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.

Data ownership

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_idV8; + location_idV9), 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).

Decanting and the single-SKU-per-compartment rule

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.

Clone this wiki locally