---
name: inventory
description: "Use when deciding how much stock to hold and when to reorder it — classifying SKUs, sizing safety stock, setting reorder points and order quantities, and tracking inventory KPIs so a business neither stocks out nor sits on dead stock. Triggers: 'what should my reorder point be', 'how much safety stock', 'run an ABC analysis', 'set up min/max levels', 'should I review weekly or continuously', 'we keep running out of X while sitting on a year of Y', 'too much cash tied up in stock', 'punt de comanda', 'stock de seguretat', 'cuánto stock de seguridad necesito si el proveedor a veces tarda el doble'. NOT raising or negotiating the purchase order itself (that is procurement)."
tags: [reorder-point, safety-stock, eoq, abc-xyz, replenishment, inventory-kpis, ddmrp, min-max]
recommends: [procurement, logistics-ops, forecasting, dashboard, reporting, spreadsheet-ops]
origin: risco
---

# Inventory

You are setting **stock policy**: how much to hold, when to reorder, how much to reorder. You are not buying it (that is `procurement`) and not moving it (that is logistics-ops). You decide the numbers that tell the buyer what to order and when.

Leave behind **two checkable artifacts** every time:

1. **Reorder-policy table** — one row per SKU: `sku, abc_class, avg_demand, lead_time, safety_stock, reorder_point, order_qty, review_mode`. This is the standing policy.
2. **Replenishment trigger list** — the subset of SKUs whose `on_hand <= reorder_point` right now, with a suggested order quantity. This is the action.

`scripts/verify.sh` checks both for shape and the structural invariants (ROP ≥ safety stock, order_qty > 0, no false triggers). Run it before you hand anything off. It validates structure, not whether the buffers are commercially smart — that judgment is yours.

## Pick the job

The flow genuinely branches. Find the row that matches the ask before doing anything else.

| Ask sounds like | Job | You produce |
|---|---|---|
| "which SKUs deserve tight control", "run ABC" | **Classify** | ABC×XYZ class per SKU → the `abc_class` column + a policy posture per class |
| "how much safety stock", "we keep running out / sitting on dead stock" | **Size the buffer** | `safety_stock` per SKU, chosen Z, chosen formula |
| "what's my reorder point", "set up min/max", "weekly or continuous" | **Set policy** | `reorder_point`, `order_qty`, `review_mode` → the full policy table |
| "is our stock policy working", "turnover", "cut dead stock" | **Track** | KPI panel against the business's own history + cycle-count cadence |

Classify before you size, size before you set policy. A uniform policy across every SKU is the original sin (anti-patterns table).

## Classify first: ABC × XYZ

Rule: **never apply one uniform service level or review mode to every SKU** — control is a budget, spend it where the value and the volatility are. Bad: 95% service on all 800 SKUs. Good: 99% on the high-value/stable AX items, make-to-order or near-zero buffer on the low-value/erratic CZ items.

- **ABC** = Pareto split by annual consumption value (`annual_demand × unit_cost`), sorted descending and cumulated. A ≈ top ~80% of value (usually ~20% of SKUs), B the next band, C the long low-value tail.
- **XYZ** = split by demand variability via coefficient of variation `CV = σ_demand / mean_demand`. X = stable (low CV), Y = fluctuating, Z = erratic.

The 9-cell matrix maps to policy: **AX** → tight automated min/max, highest service; **CZ** → manual, minimal buffer or make-to-order; **AZ** → high value but erratic, the candidate for DDMRP (below). Full cumulative-value worked sort, CV cut points, and the 9-cell→(review mode, service level, buffer posture) matrix are in `references/abc-xyz.md`.

## Safety stock

Rule: **match the formula to which thing actually varies.** Picking the demand-only formula when the supplier's lead time swings is the most common under-buffering mistake — you buffer the wrong variance and still stock out.

```text
demand varies, lead time stable:     SS = Z · σ_demand · √LT
both vary, independent:              SS = Z · √(LT · σ_demand² + avg_demand² · σ_LT²)
both vary, correlated (King):        SS = Z · σ_demand · √LT + Z · avg_demand · σ_LT
```

`Z` is set by your **target service level**, never picked by feel:

| Service level | Z |
|---|---|
| 90% | 1.28 |
| 95% | 1.65 |
| 97.5% | 1.96 |
| 99% | 2.33 |

Higher service costs disproportionately more buffer — the tail is fat, so each extra point past ~98% buys a chunk of cash you reserve for A/critical items and deny to C items. Bad→Good: supplier "usually 5 days but sometimes 12" + you used `Z·σ_demand·√LT` → switch to the independent or King formula so `σ_LT` is in the buffer. How to estimate `σ_demand` and `σ_LT` from sales/receipt history, all three formulas worked with numbers, and the diminishing-returns curve are in `references/safety-stock.md`.

## Reorder point and order quantity

These are **two separate decisions** — conflating them is the classic beginner error. ROP answers *when*, order quantity answers *how much*.

```text
ROP = (avg daily demand × lead-time days) + safety_stock        ← when to trigger
EOQ = √(2 · D · S / H)                                          ← how much to order
        D = annual demand, S = cost per order, H = annual holding cost per unit
```

EOQ minimizes ordering + holding cost under stable demand. It is an order-*size* lever and **never** a trigger — you do not "wait until you can order an EOQ." Keep `reorder_point` and `order_qty` as separate columns.

**Continuous vs periodic review** is a real branch:

| Mode | How it works | Use when | Cost |
|---|---|---|---|
| Continuous (min-max / s,S) | Order the moment on-hand hits ROP `s`, bring up to `S` | A items, perpetual stock visibility | Fastest reaction, needs live counts |
| Periodic | Check on a fixed cadence (e.g. every Friday), order up to target | B/C items, batched POs, no live system | Needs a **bigger** buffer to cover the extra review-interval exposure |

Periodic review adds `Z · σ_demand · √(review_interval)` of exposure on top of lead-time exposure — budget for it. Worked ROP/EOQ examples, the review-interval buffer adjustment, and the exact policy-table + trigger-list schemas (the columns verify.sh checks) are in `references/reorder-policies.md`.

## Demand-driven option (DDMRP)

When a static ROP keeps whipsawing on erratic demand — you re-tune it monthly and it is still wrong — switch that SKU to **buffer zones** instead of a fixed point.

- **Red** = safety/critical, sized off lead-time and demand variability.
- **Yellow** = demand coverage over the lead time.
- **Green** = order cycle / order frequency.

Trigger fires when **net flow position** = `on_hand + on_order − qualified_demand` drops from green into yellow. The zones recalculate as demand ramps, so you are not hand-editing safety stock every cycle.

Decision rule: reach for DDMRP only on **erratic (Z-class), long-lead, or strategic** SKUs — never for steady C items where a flat ROP is cheaper to run. Zone sizing and the net-flow trigger are in `references/ddmrp.md`. Set `review_mode = ddmrp` for these SKUs.

## Track it

Prove the policy works with a KPI panel — and **recompute every metric against the business's own history**; the benchmarks below are sanity rails to flag against, not numbers to hallucinate or paste as targets.

| KPI | Formula | Rough rail |
|---|---|---|
| Inventory turnover | COGS / avg inventory | ≈6–8×/yr (consumer goods) |
| Days of inventory on hand | avg inventory / COGS × 365 | ≈30–60 days |
| GMROI | gross margin / avg inventory cost | target > 2.5 |
| Sell-through | units sold / units received | ≈70–85% / mo |
| Stockout rate | stockout events / order lines | target < 5% |
| Fill rate | lines filled complete / total lines | target > 95% |
| Dead-stock flag | SKUs with zero movement over a defined window | flag, don't reorder |

**Cycle counting, not an annual full count.** Count in a rolling sequence frequency-tiered off ABC: A items often (monthly/quarterly), C items rarely (annually). The warehouse never stops, and errors on high-value items surface fastest.

## Anti-patterns

| Anti-pattern | Why it bites | Do instead |
|---|---|---|
| One uniform service level / review mode across all SKUs | Overspends on C, under-protects A | ABC×XYZ → differentiated Z and review mode |
| Demand-only SS when lead time is volatile | Buffers the wrong variance, still stocks out | Independent or King formula so `σ_LT` is in the buffer |
| Treating ROP and order quantity as one number | "Wait to order a full batch" → late triggers | Separate `reorder_point` (when) and `order_qty` (how much) columns |
| Using EOQ as a reorder trigger | EOQ is a size lever, not a signal | Trigger on ROP; size with EOQ |
| Picking Z by feel | Service level is undefined and indefensible | Set service level → read Z from the table |
| Static safety stock on erratic demand | Re-tuned monthly and still wrong | DDMRP buffer zones, `review_mode = ddmrp` |
| Annual full physical count | Warehouse stops; high-value errors found too late | ABC-tiered rolling cycle counts |
| Placing/negotiating the PO here | Wrong skill owns the buy | Hand the trigger list to `procurement` |

## Hand-offs

- Raising/negotiating the purchase order, supplier choice, payment terms, PO approval, three-way match → `procurement` (see `../procurement/SKILL.md`). You hand it the trigger list; it executes the buy.
- Warehousing, picking, packing, shipping, carriers, returns flow → logistics-ops. You set the on-hand target; it physically fulfills.
- Statistical/ML demand forecast, seasonality decomposition → forecasting. You *consume* a demand estimate and its variability; you do not own the model.
- Live KPI dashboard → dashboard. Recurring KPI report → reporting.
- Pivot tables, ABC cumulative-value sort mechanics in a sheet → spreadsheet-ops.
