Skip to main content

Putaway: what it decides and in what order

This file explains how putaway works. The other three files list every setting:

  • 01-putaway-policy-steps-and-criteria.md - the policy, its steps and its conditions.
  • 02-putaway-zone-settings.md - the putaway zone and its lot mixing rules.
  • 03-location-settings.md - the location and its lot mixing rules.

Paths to code start at crates/libs/ unless they start with crates/services/. BASELINE is crates/libs/wms-lib-sql/sql/wms-app/migrations/001_baseline.sql. Where docs in docs-obsidian/putaway_policy/ and docs-obsidian/architecture/putaway-step-screen.md disagree with this guide, trust this guide. It follows the code.

1. What putaway decides​

Putaway answers one question: "Where should this pallet go?"

  • The input is one container (a pallet or a tote) that stands in some location and holds stock.
  • The output is one location, and a reservation on it. Or no location, with a reason.
  • Putaway never moves stock itself. The operator (or the RF screen) then runs a move to the suggested location.
  • The engine moves a whole container. It does not split a pallet.

The engine is called by the RF screen PUTW03 and by the API method putaway_suggest_location (crates/services/app-server/src/web/rpcs/putaway_rpc.rs:42; roles Admin and Manager in the method table). The step editor uses a "dry run" that asks the same question and then throws the answer away.

2. The order the system works in​

Each number is a gate. A pallet must pass it to reach the next one. The code is wms-inventory/src/putaway/mod.rs, function run_steps (line 793).

  1. The request is checked. The container must exist, stand in a location and hold stock. Messages: container N not found, container N has no current location to put away from, container N holds no stock to put away.
  2. The pallet is read once (load_pallet, mod.rs:1320): its lines (owner, SKU, lot, quantity), total cube, total weight, number of stock-bearing containers, the largest Positions required, the lowest item Stack limit, and the pallet size from the pack.
  3. One policy is chosen (policy.rs:132). Each SKU uses its own Putaway Policy, or the owner default if it has none. All SKUs on the pallet must give the same policy. If not: LPN spans SKUs with different putaway policies: .... If no policy is found: no putaway policy on the SKU or the owner default.
  4. The whole policy is checked (step.rs:184, called at mod.rs:860-881). If one step cannot run, the whole policy is refused. The engine does not skip a bad step, because that would run a different policy than the one written. Examples: a step with Assigned slot or Nearest to the assigned slot; Must be empty with Consolidate; a condition with a value that does not exist.
  5. Two timers are read: the reservation time (PUTAWAY_RESERVATION_TTL_MIN, default 15 minutes) and the lock wait (5 seconds).
  6. The steps are tried in order of sequence. For each step:
    1. Guard. Is the pallet standing where the step wants? If not, skipped:guard.
    2. Pallet conditions (Product and Lot family). If any line of the pallet fails any condition, skipped:product:<Property>.
    3. Search space. The zone or location comes from the step or from the SKU. If none, skipped:no-zone or skipped:no-location.
    4. Temperature (only if the step switch is on). If the zone does not suit, skipped:temperature.
    5. Candidates. The system reads locations of the search space that pass the fixed gate (status Ok, flag None, not Discard LPN, an allowed type) and the step's Location conditions, in the step's order. It reads 20 at a time, at most 5 times: 100 candidates per step (candidates.rs:28-32).
    6. Four checks on each candidate, in this order (predicate.rs:1041):
      1. mix_intent - the step's occupancy rule (Must be empty, one SKU, one SKU and lot).
      2. consolidate - only for Consolidate steps.
      3. capacity - oversize sections, then the fit switches (cube, weight, pallet size, stack limit, quantity), then the zone case limit.
      4. landing - the location's own Mix SKU and Mix Lot flags, the lot attribute rules, and the container rules. This is the same function the move uses.
    7. The first candidate that passes all four wins the step. There is no score. Order decides.
    8. Reserve. The system locks that location (waits up to 5 seconds), asks all four checks again (another operator may have won), then writes the reservation. A pallet has at most one live reservation.
  7. If no step gives a location, the answer is "no location". The reason is no_eligible_location, or pallet_already_reserved if another request holds the pallet's reservation right now (command.rs:152-165).

After the answer:

  1. The reservation is visible under Sidebar > Inventory Balances > Pending Move-In. Other searches count its cube, weight and pallet position as taken. It lasts Putaway Reservation TTL (min) of the owner, else PUTAWAY_RESERVATION_TTL_MIN (15). A sweeper runs every 5 minutes and frees expired ones (putaway/sweeper.rs).
  2. The move is run with the reservation id. It is a "directed" move. The move checks again, always, these rules: destination type, holds on source and destination, location and zone cube and weight, zone case count, Mix SKU, Mix Lot and the lot attribute rules (wms-inventory/src/movement/validation.rs). It does not check the step's occupancy rules, Stack limit, Max quantity or pallet size. If the operator picks another place, a broken soft rule becomes a deviation that must be acknowledged (code 85). If the system setting COUNT_ON_PUTAWAY_DEVIATION is 1, the destination is queued for a count.

3. How a step is judged (short form)​

step applies? = guard matches
AND every pallet condition holds for every line
AND a search space exists
AND (temperature switch off OR zone suits every line)
candidate ok? = in the search space
AND status Ok AND flag None AND not Discard LPN AND allowed type
AND every Location condition holds
AND occupancy rule AND consolidate rule
AND capacity (all switches that are on) AND landing rules
step result = first candidate in order that is ok
policy result = result of the first step that has one

Everything inside a step is AND. The only OR is between the values of one condition ("type is Case or Pick") and between steps ("try step 10, else step 20").

4. How steps and conditions combine​

  • Steps are alternatives, in order. Put narrow steps first and wide steps last. A step with no conditions takes almost everything. The editor warns: No conditions. Every location in scope is eligible — this step matches almost everything, so it belongs near the end of the pipeline.
  • A later step cannot widen an earlier one. If step 10 would always take what step 20 takes, step 20 shows never runs / closed by step N (S).
  • Conditions are of two kinds.
    • Product and Lot conditions ask: "is this step about this pallet at all?" One failing line skips the whole step.
    • Location conditions ask: "which locations are candidates?" They cut the candidate list before any check.
  • One condition per property per step. You cannot write is and is not on the same property in one step. Use two steps.
  • is and is not are exact and case-sensitive. A list of values is OR.
  • Missing data:
    • A blank SKU field (ABC, group, class, putaway class) does not fail a pallet condition.
    • A missing attribute value (lot attribute, SKU attribute) fails it. On the location side, is removes a location with no value and is not keeps it.
  • Narrow-only fields. Location status and Location flag conditions can only repeat the fixed gate. A location on hold can never be reached by a condition.
  • A step can only narrow what the location already forbids. The location flags Mix SKU, Mix Lot, the type list and the capacity limits always apply. The step's rules apply in addition. When both speak, the stricter one wins, because both must pass.
  • Switches are independent. Each of the five Fit checks is its own yes/no. Turning one off does not turn another on.
  • A limit of 0 is not a block. An unset limit is "not checked", reported in the trace (<limit>:no_limit), and the location is still offered.

5. How a location is refused (the reasons)​

Every refusal has a short code. The dry run and the trace show the code and a count. The full list with exact texts is in 01-putaway-policy-steps-and-criteria.md, section 4.2. In short:

GroupCodesThe cause is in
Occupancy rule of the stepmix_intent, mix_intent_sku, mix_intent_sku_lotthe step (Location occupancy) and what stands or is promised in the location
Consolidatenot_consolidatablethe step Mode
Size and limitscapacity (with loc_cube, loc_weight, loc_qty, loc_positions, item_positions, loc_footprint, loc_height, zone_cube, zone_weight, zone_cases_count)location limits, zone limits, SKU and pack data
Oversizesection_none, section_not_free, section_too_small, section_promisedlocation Section, SKU Positions required
Location flags and rulesmix_sku, mix_lot, mix_attribute, lpn_multi_lotlocation flags, lot mixing rules, SKU lot rules
Raceunder_lock:<code>, lock_timeout, location_gone, pallet_already_reservedanother operator at the same moment

A location that fails the fixed gate (hold, flag, wrong type) is never a candidate, so it does not appear in any count. To see how many locations a condition removed, run the dry run: it prints Removed from the zone by this step's own conditions, before the scan: ....

6. Where the settings live​

WhatScreen / menuColumn or keyDescribed in
Policy headerSidebar > Policy > Putaway Policyputaway_policy.code, descriptionfile 01, section 1
Step fieldsstep editor (open a policy)putaway_policy_detail.*file 01, section 2
Conditionsstep editor, band Conditionsputaway_step_criterion.*file 01, section 3
Policy on a SKUSKU, tab Policies: Putaway Policysku.putaway_policy_idfile 01, 1.5
Policy default of an ownerOwner, WH Settings: Putaway Policyowner_config.default_putaway_policy_idfile 01, 1.5
Preferred zone and location of a SKUSKU, tab Policies: Putaway Zone, Putaway Locationsku.putawayzone, sku.putaway_locfile 01, 2.2
ZoneSidebar > Topology > Putaway Zonesputawayzone.*file 02
Zone lot mixing ruleszone card, list Lot Commingle Rulesputawayzone_lot_commingle.*file 02, section 3
LocationSidebar > Topology > Locationslocation.*file 03
Location lot mixing ruleslocation card, list Lot Commingle Ruleslocation_lot_commingle.*file 03, section 4
TimersSidebar > System Settings; owner WH SettingsPUTAWAY_RESERVATION_TTL_MIN, PUTAWAY_LOCK_TIMEOUT_MS, COUNT_ON_PUTAWAY_DEVIATION, owner_config.putaway_reservation_ttl_minfile 01, 4.5

Master data on the SKU that putaway reads (the SKU form, not documented here in full):

SKU field (UI label)ColumnPutaway reads it for
Putaway Policysku.putaway_policy_idchoosing the policy
Putaway Zonesku.putawayzoneZone from the SKU
Putaway Locationsku.putaway_locLocation from the SKU
Putaway Classsku.putaway_classthe condition Putaway class only
ABC Class, SKU Group, Class, Rotationsku.abc, sku_group, class, rotationthe matching pallet conditions
Positions requiredsku.positions_requiredoversize goods: needs a whole free section
Stack limitsku.stack_limitthe Stacking limit switch (lower of item and rack)
Min Temperature (°C), Max Temperature (°C)sku.min_temperature, max_temperaturethe Temperature switch
cube, gross weight, pack pallet datasku.cube, sku.gross_wgt, pack_item.pallet_*the fit switches

7. Worked examples​

The numbers use the stored units: grams, millimetres, cubic millimetres. The screens may show kg and m³. 1 kg = 1,000 g. 1 m³ = 1,000,000,000 mm³.

7.1 Pallet warehouse with FIFO​

Goal. Pallets of one SKU and one lot per bay. Top up a bay before opening a new one. Never mix lots, so FIFO picking stays clean.

Zones: RECV (receiving dock), BULK (pallet racks). SKU WIDGET: Putaway Policy = PAL-FIFO, Putaway Zone = BULK, Rotation = Fifo, cube 50,000,000 mm³ per carton, gross weight 12,000 g per carton.

Locations in BULK (all):

SettingValue
Type / Processing Type / CategoryPnd / Pallet / SelectiveShelving
Max Cube Capacity4,500,000,000 mm³ (4.5 m³)
Max Weight Capacitylevel 0: 1,000,000 g; level 3: 400,000 g
Pallet positions on floor / Stack limit2 / 1 (two pallets per bay)
Aisle, Bay, LevelA, 1..N, 0..3 (so the order is bay by bay, floor first)
Mix SKU, Mix Loton, on (the steps decide)

Policy PAL-FIFO:

StepModeScopeOccupancyFit checksWhy
10ConsolidateZone from the SKUAllow mixing, with rules: Mixed SKU = Not allowed, Mixed lot = Not allowedCube, Weight, Stacking limitTop up a bay that already holds this SKU and lot.
20SuitableZone from the SKUMust be emptyCube, Weight, Stacking limitOpen a new bay.
30SuitableZone named on this step = BULKMust be emptyCube, WeightSame as 20 for SKUs that have no zone (step 20 would be skipped:no-zone).

Walk 1. Pallet P1: 60 cartons of WIDGET, lot L-0426. Cube 3,000,000,000 mm³. Weight 720,000 g. It stands in RECV. Bay A-01-0 holds one pallet of WIDGET lot L-0426 (cube 1,000,000,000; weight 240,000).

  • Step 10 reads BULK in order A-01-0, A-01-1, ...
  • A-01-0: occupancy - the location plus the pallet is one identity (WIDGET/L-0426), pass. Consolidate - the bay holds WIDGET, pass. Capacity - weight 240,000 + 720,000 = 960,000 is not above 1,000,000. Cube 4,000,000,000 is not above 4,500,000,000. Positions: 1 standing + this one = 2, limit 2 x 1 = 2. Pass. Landing - location flags allow it. Step 10 wins. P1 is reserved for A-01-0 for 15 minutes.

Walk 2. Pallet P2: 60 cartons of WIDGET, lot L-0500.

  • Step 10, A-01-0: the bay now holds L-0426 and has a reservation for P1 (same lot). With P2 the identities are WIDGET/L-0426 and WIDGET/L-0500. Refused: mix_intent_sku_lot (step allows no mixing beyond the same sku and lot; location plus pallet would span 2 identities: owner 1/sku WIDGET/lot L-0426, owner 1/sku WIDGET/lot L-0500).
  • All other bays are empty: not_consolidatable (location holds nothing (no stock, no inbound reservation) — a Consolidate step requires the sku to already be there).
  • Step 10 ends. Step 20 reads BULK again. A-01-0 is not empty: mix_intent (step requires an empty location; occupied=true, inbound_reservations=0). A-01-1 (level 1, limit 800,000 g) is empty. Weight 720,000 is not above 800,000. Step 20 wins: A-01-1.

Walk 3. A heavy pallet of 720,000 g reaches level 3 (A-01-3, limit 400,000 g): refused capacity with the detail loc_weight: limit 400000, attempted 720000. The engine tries the next bay.

What to watch: if Max Weight Capacity is 0 on a bay, the weight check does not run. The dry run shows Offered without checking: loc_weight:no_limit on 12.

7.2 Small parts on shelving​

Goal. Totes of small parts go to pick shelves. Fill a shelf that already holds the SKU. Shelves have a volume limit and a piece limit.

Zones: SHELF (pick shelves), OVERFLOW (pallet bays for the rest). SKU SCREW-M6: Putaway Policy = SHELF-STD, Putaway Zone = SHELF, cube 2,000 mm³ per piece, weight 5 g per piece.

Locations in SHELF:

SettingValue
Type / Processing Type / CategoryPick / Piece / ShelvingStorage
Max Cube Capacity60,000,000 mm³ (60 litres)
Max quantity500
Max Weight Capacity0 (not set)
Mix SKU, Mix Loton, on

Policy SHELF-STD:

StepModeScopeOccupancyFit checksConditions
10ConsolidateZone from the SKUAny occupancyCube, QuantityLocation type is Pick
20SuitableZone from the SKUAny occupancyCube, QuantityLocation type is Pick
30SuitableZone named on this step = OVERFLOWMust be emptyCube, WeightLocation type is Case

Walk. Tote T1: 300 pieces of SCREW-M6 (cube 600,000 mm³, weight 1,500 g). Shelf S-01-01 holds 150 pieces (cube 300,000).

  • Step 10, S-01-01: consolidate - it holds SCREW-M6, pass. Quantity: 150 + 300 = 450, limit 500, pass. Cube: 900,000, limit 60,000,000, pass. Wins.
  • If the shelf held 380: 380 + 300 = 680. Refused: capacity with loc_qty: limit 500, attempted 680.
  • Shelves with Max quantity = 0 are not refused. The dry run shows loc_qty:no_limit.

What to watch:

  • The Quantity check counts stock only. It does not count pieces promised to another tote by a live reservation. Two totes asked one after the other can both be sent to a shelf that holds 380 and together exceed 500. Cube, weight and pallet positions do count reservations. (space.rs:732-742; the reservation row has cube and weight but no quantity.) The move does not check Max quantity either. Use a short reservation time on pick faces.
  • Max Weight Capacity is 0, so even if Weight were on it would not run.
  • Step 30 refuses nothing on weight if the pack has no weight: loc_weight:no_stock_weight.

7.3 Mixed lots and restrictions​

Goal. Food with an allergen attribute. Allergen lots must not share a pallet with non-allergen lots. Allergen goods go to their own zone.

Zones: FOOD, ALRG. Lot policy of the food SKUs has the attribute ALLERGEN (values YES or NO). Rules:

RuleSetting
Zone ruleZone FOOD, Lot Commingle Rules: Attribute Code = ALLERGEN, Allow Mix off
Location exceptionLocation F-09-01, Lot Commingle Rules: ALLERGEN, Allow Mix on
Location flags in FOODMix SKU off (one product per bay), Mix Lot on
Location in ALRGALRG-01: Mix SKU off, Mix Lot on

Policy FOOD-STD:

StepScopeConditionsOccupancyOther
10Zone named on this step = ALRGLot attribute ALLERGEN is YESMixed SKU = Not allowed, Mixed lot = AllowedFit: Cube, Weight
20Zone from the SKU (FOOD)SKU class is AMBIENTMixed SKU = Not allowed, Mixed lot = Not allowedFit: Cube, Weight; Temperature on
30Zone named on this step = FOODnoneMust be emptyFit: Cube, Weight

Walk 1. Pallet P1: SKU PEANUT, lot F-100, ALLERGEN = YES.

  • Step 10: the pallet condition holds. The search space is ALRG. ALRG-01 is empty. Occupancy: one identity, pass. No lot rule on ALRG, pass. Wins: ALRG-01.

Walk 2. Pallet P2: SKU RICE, lot F-200, ALLERGEN = NO. F-01-0 holds RICE lot F-150.

  • Step 10: the lot has ALLERGEN = NO, so the condition fails. Status skipped:product:LotAttribute.
  • Step 20: the SKU class matches. F-01-0: occupancy - two identities (RICE/F-150, RICE/F-200). Refused mix_intent_sku_lot. F-01-1 is empty. Landing: Mix SKU off is fine (nothing else there). The zone rule ALLERGEN does not refuse because the pallet has one lot. Wins: F-01-1.

Walk 3. Pallet P3: SKU MIX1, lot F-300 (ALLERGEN = YES) and lot F-301 (ALLERGEN = NO) on one pallet.

  • Step 10: one line has NO, so the whole step is skipped (skipped:product:LotAttribute).
  • Step 20: in FOOD, LandingRule asks the zone rule. ALLERGEN has two values on the pallet and Allow Mix is off, so every FOOD location is refused with mix_attribute (attribute ALLERGEN=YES mixing not allowed for lot F-300 on lpn Some("LPN3") [Some("putawayzone_lot_commingle rule on zone FOOD")]). Except F-09-01, which has its own rule Allow Mix on. The location rule wins over the zone rule.
  • Note: this rule reads stock rows that sit on the landing container itself. See the "To verify" block in 02-putaway-zone-settings.md, section 3.3, for pallets whose stock sits on child cases.

Walk 4. A pallet with two different SKUs fails step 20 and step 30's occupancy at every location: step 20 asks "one SKU", and the pallet alone is two (mix_intent_sku). Step 30 (Must be empty) can still place it.

8. "No location" checklist​

Open the policy and run Dry run on the LPN. Read the sentence for each step.

You seeDo this
The engine would not evaluate this pallet: no putaway policy ...Set Putaway Policy on the SKU or the owner.
... spans SKUs with different putaway policies ...Put the pallet's SKUs on one policy.
step uses scope=AssignedSlot ...Change that step.
skipped:no-zone on every stepSet Putaway Zone on the SKU, or add a step with Zone named on this step.
skipped:product:<Property>The pallet fails that condition. Check the SKU field, or the lot attribute value.
skipped:guardThe pallet is not standing where the guard wants.
skipped:temperatureFix the zone range, the SKU range, or switch Temperature off.
ran over 0 candidate(s)The zone has no location that is Ok, flag None, not Discard LPN and of an allowed type. Check status (holds), flags and types.
Refused by: capacity on NRead the Detail column. It names the limit. Raise it, set the item data, or use another zone.
Refused by: mix_sku, mix_lotThe location flags forbid the mix. Change the flag or choose another location.
Refused by: mix_intent...The step rule is stricter than you meant.
It stopped at its scan budget ...More than 100 candidates in the zone were tried. Narrow the step with conditions, or split the zone.
Offered without checking: ...:no_limitA switch is on but the limit is 0. Fill the limit or switch it off.

9. Known gaps and surprises (found while writing this guide)​

These are real behaviours of the current code. File and line are given.

  1. The step editor offers two values that do not work. Assigned slot and Nearest to the assigned slot are shown switched off (not implemented yet), but the API can still store them, and then the engine refuses the whole policy (step.rs:234-257).
  2. Pick route does not use Route Sequence. It orders by aisle, bay, level, code (location.rs:420). Aisle is text.
  3. The step readback sentence ignores two fit switches (Stacking limit, Quantity) and says "Checks nothing physical" when only those are on (putaway_step_describe.rs:1047-1066).
  4. The Temperature label says "cover" and "location". The code tests overlap of the zone range with the SKU range (step.rs:458-469). The dry run says "suit" and is right.
  5. Turning Cube or Weight off in a step does not stop the move from checking them (movement/validation.rs:960-1000). Putaway may offer a place the move then refuses.
  6. Quantity ignores quantity promised by reservations. Cube, weight and positions do not (space.rs:732-742).
  7. Location Mix SKU and Mix Lot do not see reservations. Only the step rule Mixed SKU / Mixed lot does (inventory_service.rs:999-1091, predicate.rs:534-598).
  8. Stored only fields. Zone: Inbound Location, Outbound Location, RF Pick Strategy, Replenishment Allowed, Replenishment Threshold, Default Label Printer, Default Report Printer, Auto Print Addr Label, Auto Print Carton Label. Location: Cube, Check Digit. SKU Putaway Class = NoMix does nothing unless a step has a Putaway class condition.
  9. Stale comments. predicate.rs:197-218 says putaway does not exclude Discard LPN locations, but the candidate SQL does (location.rs:2143). The column comment on location.loc_flag says moves need flag None, but moves ignore flags (validation.rs:544-575). docs-obsidian/putaway_policy/README.md says zone lot rules have no reader. They do (inventory_service.rs:1537-1555).
  10. Allocation still allocates from Lost locations (inventory_balance_atp.rs:338-343, open point in the code comment).
  11. Generate does not set aisle or bay, so putaway order for generated locations is by level then code (03-location-settings.md, section 5).
  12. Zone Description is never NULL, so the step editor's zone list can show an empty name (wms-meta-ui baseline, zones source).