SOP: Receiving Putaway
Document ID: WMS-REC-004 Version: 1.0 Effective date: 04/30/2026 Owner: Warehouse Operations Manager Next review: [six months from effective date] Applies to: Floor staff moving newly-received inventory from the receiving zone to its pickable bin
1. Purpose
This procedure governs the physical movement of inventory from the receiving location (where approval per WMS-REC-003 placed it) to its final pickable location in the warehouse. Putaway is the step that turns "received and signed off" stock into "findable and pickable" stock. Until putaway is complete, the stock is technically AVAILABLE in the system but sitting in the receiving zone — pickers can be allocated to it (the FEFO/FIFO policy in WMS-PICK-001 §5 doesn't care that it's still in receiving), and they will end up walking to the receiving zone to pick from it. That defeats the purpose of having pick zones.
Approval creates a WorkTask of type PUTAWAY with one TaskItem per inventory line. This SOP covers what should happen with that task next.
⚠ MAJOR CURRENT-STATE GAP. As of WMS app v[current], the receiving-side putaway flow is substantially unimplemented:
- The
WorkTaskof typePUTAWAYis created at approval (WMS-REC-003 §4.2).- No UI route exists for working a receiving-side putaway task. The only putaway pages in the app are
/returns/putaway/:taskIdand/returns/customer-putaway/:taskId— both for return-flow putaway, not receiving.- No API endpoint specific to receiving putaway exists. The generic
WorkTaskServiceexists but no Fastify route exposes its methods for floor use.- Crucially,
WorkTaskService.complete()does not move inventory. It updates order status only forPICKINGtasks. For aPUTAWAYtask, "complete" today means the row'sstatusflips toCOMPLETED— and nothing else. NoInventoryUnit.locationIdupdates, no per-unit move events, no audit trail of where the stock physically ended up.This SOP documents (a) the intended target flow when the system is built, and (b) the current-state workaround that uses WMS-INV-001 §4.1 (Move Location) on each unit to physically log the move. Putaway tasks created at approval are effectively idle today.
2. Scope
In scope:
- The
WorkTaskof typePUTAWAYcreated at receiving-approval time - Moving each newly-received
InventoryUnitfrom the receiving zone to its pickable bin - Recording the moves so the audit trail is intact
- The current-state workaround using WMS-INV-001
Out of scope:
- Customer return putaway — see WMS-RET-003 (separate codepath, separate task type)
- Bin location creation (when a SKU has no current pickable bin) — see WMS-INV-004
- Floor-count and SKU-to-location mapping — see WMS-INV-003
- Pick allocation to pickable bins — see WMS-PICK-001
3. Roles & permissions
API enforcement: Today, none of the receiving putaway endpoints exist, so there is nothing to enforce. The downstream operation that the workaround actually uses —
POST /inventory/:id/move— checks authentication only (see WMS-INV-001 §3). Any authenticated user can move any unit.
| Role | Work a putaway task | View putaway tasks |
|---|---|---|
| READONLY | — | ✓ |
| STAFF | ✓ | ✓ |
| MANAGER | ✓ | ✓ |
| ADMIN | ✓ | ✓ |
| SUPER_ADMIN | ✓ | ✓ |
Operational expectation: putaway is high-throughput and assigned to whoever's free. The WorkTask is created with status: PENDING and priority: 50; it sits in the queue alongside picking tasks until someone takes it. Pickers and putaway staff are typically the same people on a small floor.
4. Procedures
4.1 The target putaway flow (when the system is built)
This describes the intended state. See §4.2 for the current-state workaround.
Use when: A receiving session has been approved, you're on putaway duty, and the resulting WorkTask is in PENDING.
Prerequisites:
- The
WorkTaskexists withtype: PUTAWAY,status: PENDING, and a non-zerototalItems. - Each linked
TaskItemhas aproductVariantId, a sourcelocationId(the receiving zone), and aquantityRequiredmatching the approved good quantity. - A pickable destination location exists for each variant. (If not, see WMS-INV-003 for the new-SKU mapping flow before starting putaway.)
Steps (target):
- Open the work-task queue. Filter by
type: PUTAWAY. - Tap the task you want to work. Tap Assign to me (or whatever the queue UI calls self-assignment).
- Tap Start. Task status flips to
IN_PROGRESS. - For each
TaskItemin sequence: a. The screen shows: SKU, current location (receiving zone), quantity to put away. b. Walk to the receiving zone. Locate the units. c. Scan the source location barcode to verify (UI confirms). d. Walk to the destination bin (the system suggests one based on prior placement; if no suggestion, see WMS-INV-003). e. Scan the destination location barcode. f. Scan a unit barcode (or +1) for each unit moved. g. Tap Done for the line. - Once every
TaskItemis complete, the task auto-completes. Task status flips toCOMPLETED.
Result (target):
- Each
InventoryUnitfor the moved variants now haslocationId: <destination bin>. - One
inventory:unit_movedevent per unit moved is logged ininventory_eventswith the From/To locations and your user. - The
WorkTaskrow isCOMPLETED. Atask_eventsrow is logged witheventType: TASK_COMPLETED. - The receiving zone empties (modulo what's still on the dock).
4.2 Current-state workaround: moves via WMS-INV-001
Use when: §4.1 doesn't work because the UI is unbuilt, but stock has been approved and is sitting in the receiving zone.
The workaround uses the Move Location action documented in WMS-INV-001 §4.1 on each unit individually. This produces the correct audit trail (inventory:unit_moved events with From/To) but does not mark the receiving putaway WorkTask as COMPLETED — that row remains PENDING forever.
Steps:
- After approval, note the putaway task number from the success toast (
PUTAWAY-{poRef}-{shortId}). Write it down or screenshot it for your putaway handoff log (§5.3). - Walk to the receiving zone and physically locate the new stock by SKU.
- For each new
InventoryUnit(one per variant + lot, created by the approve transaction): a. From any device, navigate to/inventory/:unitIdfor that unit. The fastest path: from the approval page, tap the SKU in the line list (links to inventory). On the inventory page, the new unit is at the top, sorted bycreatedAt. b. Follow WMS-INV-001 §4.1 to move the unit to its pickable bin location. Scan the destination, leave quantity blank for full move. c. The system writes aninventory:unit_movedevent. The unit'slocationIdupdates. - Repeat for every unit created at approval.
- At the end of the session: in
#warehouse-ops, post a message tagging the warehouse manager:This bridges the audit gap left by the un-completedPUTAWAY DONE — {putawayTaskNumber}Approved session: {sessionId}Units moved: {count}Putaway by: {your name}Time: {ISO timestamp}WorkTaskrow.
Result:
- All inventory is at its final pickable location, with
inventory:unit_movedevents giving full traceability. - The
WorkTaskof typePUTAWAYis stillPENDINGin the database. This is intentional with the current code — completing it via the work-task service would not do anything additional and would mask the fact that the proper flow doesn't exist. Leave it pending; engineering can sweep these once the proper UI ships.
⚠ This workaround leaves
PUTAWAYtasks accumulating asPENDINGrows inwork_tasks. That is the visible signal that putaway needs a proper implementation. If your queue page filters outPUTAWAYtasks (because they're noise), you'll lose the signal. Better to accept the noise and feel the friction.
4.3 What "complete" means today (and what it doesn't)
If you or anyone else calls WorkTaskService.complete(taskId) for a putaway task today, here is exactly what happens:
WorkTask.statusflips fromIN_PROGRESS(or whatever) toCOMPLETED.- A
task_eventsrow is created witheventType: TASK_COMPLETED, your user ID, and the timestamp.
Here is exactly what does not happen (compared to a real putaway implementation):
- No
InventoryUnit.locationIdupdates. - No
inventory_eventsrows of typeinventory:unit_movedare written. - No verification that the inventory was actually moved physically.
This is why §4.2's workaround uses POST /inventory/:id/move directly — it's the only path that produces the correct inventory side effects today. Calling WorkTaskService.complete() in isolation produces an audit lie ("task was completed") with no inventory truth attached.
⚠ Do not call
completeon a receiving putaway task as a shortcut. ACOMPLETEDtask with stock still in the receiving zone is worse than aPENDINGone — the next person checking putaway state will think it's done. Leave the taskPENDINGand complete the moves via §4.2.
4.4 Where putaway exists today (return flow)
For comparison and to avoid confusion: the return flow (/returns/putaway/:taskId and /returns/customer-putaway/:taskId) does have a working putaway UI. That code path:
- Is a separate route file (
return.routes.tsandcustomer-putaway.routes.ts). - Operates on the
ReturnandCustomerReturnPutawayTasktables, not onWorkTask. - Updates inventory via
inventoryUnitcreate/update calls inline within the return controllers, not viaWorkTaskService.
Lessons from that code path will inform a proper receiving-putaway implementation. See WMS-RET-003 for the customer-side equivalent procedure.
5. Reference
5.1 Putaway task structure
The putaway WorkTask created by approval has these fields (from WorkTaskService and the approve transaction in ReceivingService.approve()):
| Field | Value |
|---|---|
taskNumber | PUTAWAY-{poReference}-{base36-timestamp} |
type | PUTAWAY |
status | PENDING (until/unless completed) |
priority | 50 |
totalItems | Count of approved lines with goodQuantity > 0 |
completedItems | 0 |
totalOrders | 0 (putaway is not order-driven) |
orderIds | [] |
notes | Put-away for PO {poReference} |
Each TaskItem row has:
| Field | Value |
|---|---|
taskId | The putaway task ID |
productVariantId | The received variant |
locationId | The receiving location ID (source) |
quantityRequired | The good quantity (counted minus damaged) |
quantityCompleted | 0 (until/unless completed) |
sequence | 1, 2, 3... in the order processed by the approve transaction |
status | PENDING |
5.2 What a real putaway implementation should write
When the proper flow is built, completing a putaway task should produce:
- One
inventory:unit_movedevent per unit moved (From: receivingLocation,To: destinationBin, withuserId) InventoryUnit.locationIdupdated for each moved unitTaskItem.quantityCompletedandTaskItem.locationIdupdated to reflect destinationWorkTask.status: COMPLETEDandWorkTask.completedItems: totalItemstask_eventsrow witheventType: TASK_COMPLETED
5.3 Putaway handoff log (current-state)
Until the proper flow ships, maintain a putaway handoff log. The Slack post in §4.2 step 5 is the log. Format:
PUTAWAY DONE — PUTAWAY-{poRef}-{shortId}
Approved session: {sessionId}
Units moved: {count}
Putaway by: {name}
Time: {ISO timestamp}
Manager weekly review: scrape #warehouse-ops for PUTAWAY DONE posts and reconcile against the count of PUTAWAY WorkTask rows in status PENDING from the past week. The number of PENDING rows should equal the number of approved sessions in that week. If not, an approval went without a putaway — investigate.
5.4 Related SOPs
- WMS-REC-001 — Counting (the start of the inbound flow)
- WMS-REC-002 — Exceptions (which determine
goodQuantityper line) - WMS-REC-003 — Approval (which creates the putaway task)
- WMS-INV-001 §4.1 — Move Location (the workaround procedure)
- WMS-INV-003 — Floor count & location discovery (when a destination bin must be created)
- WMS-INV-004 — Location management (creating bins)
- WMS-PICK-001 — Picking (the immediate downstream consumer of putaway-completed inventory)
- WMS-RET-003 — Customer return putaway (the code path that does work, for reference)
6. Audit & compliance
The current-state workaround produces correct inventory audit data via inventory_events (inventory:unit_moved per unit). What it lacks:
- No association between the move events and the source PO. The
inventory_events.payload.reasonfield isnullfor moves performed via the Move Location button (see WMS-INV-001 §4.1 callout). The PO context is implicit — provable only via timestamp correlation with theaudit_logsSESSION_APPROVEDrow. - No completion of the
WorkTask. The task row staysPENDING, which is the visible flag that this flow is broken. - Reliance on a Slack-message handoff log rather than a structured database record.
When the proper flow ships, the audit story becomes:
task_eventsrows tie each move action to a putaway task to a PO viaWorkTask.taskNumberInventoryUnithistory is queryable per-unit via the Activity Feed (WMS-INV-001 §4.4)- Duration from approval to putaway-complete becomes a meaningful metric
Manager weekly review (current-state):
- Count of
WorkTask WHERE type='PUTAWAY' AND status='PENDING' AND createdAt < now() - 24h— anything older than 24 hours is stale; reconcile via §5.3. #warehouse-opsPUTAWAY DONEposts — verify each one has a matchingPUTAWAY-...task in the database.
Manager weekly review (target-state):
- Mean time
(WorkTask.completedAt - WorkTask.createdAt)for putaway tasks. If creeping above ~2 hours, putaway is bottlenecked. - Receiving zone unit count. Should trend toward zero by end-of-day; persistent backlog is a putaway-staffing issue.
7. Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
| I approved a session but I don't see a putaway task in any UI | Putaway UI doesn't exist | Use §4.2 workaround. Move each unit via WMS-INV-001 §4.1. |
| Pickers are walking to the receiving zone to pick orders | Stock was approved but never put away | Audit: InventoryUnit WHERE locationId = <receiving location> AND createdAt < now() - 4h. Each one is a missed putaway. Run §4.2 retroactively. |
Multiple PUTAWAY tasks accumulating in PENDING status | Expected with current implementation | These are the audit signal that the proper flow is unbuilt. Don't bulk-cancel them; engineering will sweep them when the flow ships. |
Tried to call WorkTaskService.complete() on a putaway task — task says complete but stock didn't move | Per §4.3, complete() doesn't move inventory for putaway tasks | Manually move via §4.2. Document in #warehouse-ops. |
| The destination bin has no barcode | Unmapped location | See WMS-INV-003 (floor count) to map a SKU to a bin and assign a barcode before completing putaway. |
| Approved session was for a SKU we don't normally stock; no destination bin exists | New SKU first time | Use WMS-INV-003 to create the bin and barcode it, then run §4.2. |
Slack PUTAWAY DONE message has typos or a wrong task number | Currently the only audit; no validation | Manager reconciles by hand. Discipline > tooling, until the UI ships. |
8. Escalation
- Inventory has been at the receiving location for >24 hours: warehouse manager. Either putaway staff is overloaded or §4.2 isn't being run. Both are operational issues, not bugs.
- Engineering priority for the receiving putaway UI: this is the highest-leverage missing receiving feature. Until it ships, every approved session generates manual work and a Slack post. Loop in the warehouse operations manager to advocate.
- Receiving location is full and no more POs can be received: stop receiving. Run §4.2 on everything in the zone. Resume receiving only when the zone has capacity.
- Pickers report walking to the receiving zone repeatedly: a serious signal that putaway is failing. Audit per §7 row 2.
9. Revision history
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | [DATE] | [NAME] | Initial release. Documents the receiving-putaway flow as designed and the current-state code gap: the WorkTask of type PUTAWAY is created at approval but no UI, no API route, and no WorkTaskService codepath actually moves inventory for it. WorkTaskService.complete() is explicitly documented as a "do nothing for putaway" function — it flips status without moving inventory. The §4.2 workaround uses POST /inventory/:id/move from WMS-INV-001 to produce the correct inventory:unit_moved events. The §5.3 Slack handoff log bridges the audit gap until the flow is built. Cross-references the working returns putaway code paths (/returns/putaway/:taskId, /returns/customer-putaway/:taskId — see WMS-RET-003) as the reference implementation that should inform the receiving build. Field structure of the putaway task pulled from ReceivingService.approve() in packages/domain/src/services/receiving.service.ts. |