Skip to main content

A shelf that has to be restocked

A grocery aisle has stock in two places: on the shelf, where shoppers can reach it, and in the storeroom, where it waits for someone to put it out. A sale is lost either way — and the two losses have different owners and opposite remedies.

an article iswhose problemwhich lever
on the shelf——
not on the shelf, but in the storeroomshelf-stockinganother stacker
not ordered at allpurchasinga higher reorder point

⚠️ A model with one store cannot tell those apart. It has one stockouts counter, and a manager reading it cannot know whether to hire somebody or to phone the supplier.

Any unfamiliar word is in the glossary.

The shape​

Two stores, and a restock task that moves stock from one to the other:

⭐ The restock is an ordinary product with an ordinary route. It queues for the stacker like any other work, so its lead time is an outcome of the model rather than a number somebody typed — which is the whole reason a second stacker can change the answer.

The shelf is filled by a task rather than by a purchase order:

id: shelf
capacity: 240.0
initial: 240.0
reorder_at: 120.0
max_outstanding: 2
replenished_by: { kind: task, product: restock }

and the step that does the moving both draws and puts back:

resource: stacker
process_seconds: { kind: constant, value: 600.0 }
consumes:
- consumable: storeroom
quantity: { kind: constant, value: 12.0 }
produces:
- consumable: shelf
quantity: { kind: constant, value: 12.0 }

The model​

# yaml-language-server: $schema=https://clockwright.dev/schema/model.json
name: grocery-aisle
tick_scale_per_second: 1000
horizon_seconds: 1209600.0
warmup_seconds: 86400.0
resources:
- { id: till, servers: 3 }
- { id: stacker, servers: 2 }
consumables:
- id: shelf
capacity: 240.0
initial: 240.0
reorder_at: 120.0
max_outstanding: 2
replenished_by: { kind: task, product: restock }
- id: storeroom
capacity: 8000.0
initial: 8000.0
reorder_at: 4000.0
reorder_quantity: 6000.0
lead_time_seconds: { kind: constant, value: 43200.0 }
products:
- id: shopper
steps:
- resource: till
process_seconds: { kind: exponential, mean: 95.0 }
consumes:
- consumable: shelf
quantity: { kind: constant, value: 1.0 }
- id: restock
steps:
- resource: stacker
process_seconds: { kind: constant, value: 600.0 }
consumes:
- consumable: storeroom
quantity: { kind: constant, value: 12.0 }
produces:
- consumable: shelf
quantity: { kind: constant, value: 12.0 }
releases:
- product: shopper
interarrival_seconds: { kind: exponential, mean: 40.0 }

model_hash: 264bb3198400d9ae

What it says​

Two weeks, one warm-up day, development laptop (AMD Ryzen 7 7840U). Three shops, differing in one thing each:

shelf stockoutsmean wait for stockstoreroom stockoutsstoreroom low-waterstacker busysold
as written00 s02,88862.2%32,605
⚠️ one stacker30,086131,550 s03,128100.0%26,207
⚠️ reorder_at 200 in the storeroom8,9434,580 s10861.9%32,574

⭐⭐ The middle row is the point​

With one stacker the shop loses 6,398 sales, and the storeroom never runs short — it still holds 3,128 units at its lowest. The stock was in the building. Nobody put it on the shelf.

⚠️ A single-store model reports that as a stockout, which reads as a purchasing failure and would send a manager to raise the reorder point — the one lever that cannot help. The stacker is at 100%; the answer is another pair of hands.

⚠️ And the third row shows why you must read both stores​

A storeroom that is ordered too late also empties the shelf: 8,943 shelf stockouts, because a restock task that cannot get stock out of the storeroom cannot put any on the shelf. The shelf's own numbers look like the second row all over again.

⭐ What separates them is the storeroom: 10 stockouts and a low-water mark of 8 units against 3,128. And the stacker is only 62% busy, so it is plainly not the constraint. Two shelves that look equally empty, two different phone calls.

tip

The general rule. When a resource fills a store and something else drains it, ask which of them ran out. min_level is the field that answers it — a store that ran dry at hour three and refilled by hour four ends the run looking exactly like one that never ran short.

What to try​

  • Give the one-stacker shop a third till. Nothing improves: the tills were never the constraint, and the model says so by leaving till utilisation where it was.
  • Raise the shelf's reorder_at from 120 to 200. The stacker starts earlier and the shelf holds up longer — at the cost of more trips for the same stock.
  • Set max_outstanding to 1. ⚠️ The second stacker stops helping entirely, because only one restock trip is ever in flight. That field is how many replenishments may be outstanding, and with tasks it is what decides whether extra people can be used at all.