Replay Engine¶
Table of Contents¶
- Overview
- Core Concepts
- Creating a Replay Run
- Running a Replay
- Replay Dashboard
- Stock Over Time
- Fill Rate Over Time
- Iteration Details
- SKU Metrics
- Worst Offenders
- SKU Drill-Down Detail
- Cancelling and Deleting
- Limitations and Performance
Overview¶
The Replay Engine re-runs the supply + allocation planning pipeline iteratively over a configurable historical horizon. At each iteration, the engine advances the "planning date" by one step (daily or weekly), receives supply orders that have arrived by that date into on-hand inventory, and runs the full supply and allocation engines again.
This allows you to answer questions like:
- What would our fill rate have been if we had used this planning configuration over the past year?
- How would stock have evolved week by week under the current policy?
- Where did backorders accumulate, and when did they clear?
The replay uses the same engines as the live planning process — not a simplified simulation — so the results accurately reflect what the system would have produced at each point in time.
Where to find it
The Replay screen is accessible from the 🔄 Replay link in the sidebar's Action group.
Core Concepts¶
Replay Run¶
A replay run defines the scope of a replay:
| Parameter | Description |
|---|---|
| Source pipeline | The pipeline whose configuration (supply scenario, allocation parameters, forecast) will be replayed |
| Start date | The first planning date — the beginning of the historical horizon |
| End date | The last planning date — the end of the historical horizon |
| Granularity | Step size: weekly (default) or daily |
Each replay run creates its own replay pipeline (a copy of the source pipeline's configuration). The source pipeline's data is never modified.
Iteration¶
Each iteration is one complete planning cycle at a specific planning date:
- Receive supply: Firm supply orders that arrived since the previous iteration are "received" — their quantity is added to on-hand inventory (delta receive, so each order is received exactly once)
- Run supply planning: The full supply engine runs, generating new replenishment orders
- Run allocation: The allocation engine distributes available supply across demands
- Record metrics: Fill rate, total stock, and shortage are captured
Each iteration writes its output with archive_date = Monday of the planning week (the project-wide archive convention), so results are naturally historized and queryable across iterations.
Weekly granularity is Monday-aligned
Weekly iterations always fall on Mondays. The supply and allocation engines stamp every PIPE_* row with the Monday of the planning week, so the engine aligns iteration dates to the same convention — a run started mid-week begins on the Monday of that week.
Iteration dates and on-hand inventory¶
- Orders that arrived before the replay's first iteration are assumed to already be reflected in the on-hand snapshot and are never re-received.
- Orders arriving between two iterations are received into on-hand on the first iteration whose planning date is on/after the arrival — exactly once (each receipt is journaled).
- Replays are side-effect-free: every on-hand delta applied during the run (receipts, and shortfall carries in Backorder mode) is journaled and reversed when the run ends — on success, failure and cancellation alike. The tenant's on-hand ends exactly where it started; the stock evolution lives in the per-iteration metrics. A crashed process leaves the journal behind, and the next run start (or the API's startup sweep) reverses it automatically.
- Lost sale vs backorder:
demand_carry_modein the run config controls unfilled demand. Withlost_sale(default) unallocated demand disappears; withbackorderthe shortfall is subtracted from on-hand so the next iteration sees the backlog.
Stock Evolution¶
Between iterations, the on-hand inventory evolves realistically:
- Supply orders from previous iterations that arrive by the current planning date are received into stock
- Demand that was allocated (filled) reduces available inventory
- Unfulfilled demand (backorders) carries forward
Creating a Replay Run¶
- Navigate to the Replay screen (🔄 in the sidebar)
- Click + New Replay
- Fill in the form:
| Field | Required | Description |
|---|---|---|
| Name | Yes | A descriptive name for this replay run |
| Description | No | Optional details |
| Source Pipeline | Yes | The pipeline to replay |
| Start Date | Yes | First iteration date (e.g. 2025-06-01) |
| End Date | Yes | Last iteration date (e.g. 2026-06-01) |
| Granularity | Yes | Weekly (default) or Daily |
| Demand Carry Mode | Yes | lost_sale (default) — unfulfilled demand disappears; backorder — shortfall carries into the next iteration's on-hand |
- Click Create

The system creates a new replay pipeline (marked with is_replay = TRUE) that clones the source pipeline's supply and allocation scenario configuration. The source pipeline is not modified.
Running a Replay¶
After creating a replay run, its status is pending. To start it:
- Click the Start button in the runs table
- The status changes to running and the progress bar begins advancing
The replay runs in the background — you can navigate away and return later. The progress bar shows the current iteration out of the total.
Runtime estimate
Each iteration takes approximately 1 minute (supply + allocation). A 52-week replay runs in about 1 hour. A 365-day daily replay takes approximately 6 hours.
Replay Dashboard¶

When you select a running or completed replay run, the dashboard shows a tab bar with three views:
- Overview — the original panels: run config badges, Stock Over Time chart, Fill Rate & Shortfall chart, and the Iteration Details table
- SKU Metrics — a sortable, searchable per-(item, site) grid of target vs. achieved fill rate and stock start/end
- Worst Offenders — the SKUs that increased stock over the replay and the SKUs that missed their target fill rate
The panels of the Overview tab:
Stock Over Time¶
A line chart showing projected inventory across all iterations. Each line represents one item-site combination, with the X-axis showing planning dates and the Y-axis showing stock quantity.
Use this to identify:
- Periods of low stock or stockouts
- Inventory build-up patterns
- Seasonal stock patterns under the replayed policy
Fill Rate & Shortfall Over Time¶
A combined chart with:
- Fill rate % (left Y-axis, line chart): the percentage of demand that was allocated
- Shortfall qty (right Y-axis, bar chart): the total unfulfilled quantity
Use this to identify:
- Weeks where allocation was insufficient
- Trends in service level
- The impact of lead times on fill rate
Iteration Details Table¶
A row-by-row breakdown of every iteration:
| Column | Description |
|---|---|
| # | Iteration number |
| Planning Date | The date this iteration simulated |
| Status | Completed, failed, or skipped |
| Fill Rate | Percentage of demand allocated |
| Total Stock | Sum of projected on-hand across all item-sites |
| Shortage | Total shortage quantity |
| Duration | Wall-clock time for this iteration |
Stock Over Time¶
The stock evolution chart queries PIPE_supply_inventory_projection from ClickHouse, grouped by archive_date (one per iteration). Each series is an item-site combination.
The chart supports:
- Hovering to see exact values
- Zooming by clicking and dragging
- Double-clicking to reset the zoom
Fill Rate Over Time¶
Fill rate is the overall fill rate of the allocation run — allocated quantity across all demands, divided by the total requested quantity:
allocated_total = SUM(allocated_qty) FROM PIPE_allocation_results -- every allocated unit
requested_total = SUM(requested_qty) FROM PIPE_allocation_unfulfilled -- shortfall demands
+ (allocated_total - SUM(allocated_qty) FROM unfulfilled) -- fully-filled demands
fill_rate = allocated_total / requested_total
PIPE_allocation_unfulfilled only holds demands that ended short, so computing allocated/requested from that table alone would report the fill rate of the shortfall subset rather than of the whole run. Both tables are partitioned by (pipeline_id, archive_date), so each iteration's fill rate is computed independently.
Iteration Details¶
The iteration table reads from the replay_iteration PostgreSQL table. Each row contains a metrics JSONB column with the following structure:
{
"total_stock": 15420.0,
"total_shortage": 320.5,
"avg_stock": 128.5,
"total_allocated": 15100.0,
"total_requested": 15420.0,
"total_shortfall": 320.0,
"fill_rate": 0.9792
}
SKU Metrics¶
The SKU Metrics tab shows one row per (item, site) that appears anywhere in the replay:
| Column | Description |
|---|---|
| Item / Site | The item-site combination |
| Segment | The binding segment (the one whose target applies) |
| Target % | The segment's MEIO fill-rate target, from the source pipeline |
| Achieved % | The fill rate achieved at the selected archive (iteration) |
| Diff (pp) | Achieved − target, in percentage points (red when negative) |
| Stock Start | Projected inventory at the first iteration (week 0) |
| Stock End | Projected inventory at the last iteration (final week) |
| Δ Stock | Stock End − Stock Start (red when stock grew) |
| Shortage | Total shortfall quantity at the selected archive |
Controls:
- Archive selector — pick the iteration whose fill columns (Target/Achieved/Diff/Shortage) you want. Defaults to the last iteration. Stock Start/End are replay-level and never change with the selector.
- Search — case-insensitive substring filter on item or site name.
- Sort — click any column header to toggle ascending/descending. Default sort is Diff (pp) ascending (worst first); rows with no value always sort last.
- Row click — opens the SKU drill-down detail for that (item, site) at the selected archive.
Where the target comes from
Replays do not re-run segmentation or MEIO, so the target fill rate is policy from the source pipeline: the SKU's latest segment membership → the highest MEIO group target across the SKU's segments (the binding group). SKUs without explicit membership use the default segment's target; SKUs with no resolvable target show — in the Target % column.
The achieved fill rate uses the project-standard overall formula across both allocation tables (see Fill Rate Over Time) — the unfulfilled table alone holds only the shortfall subset.
API: GET /api/replay/runs/{id}/sku-metrics?archive_date=YYYY-MM-DD — all rows are returned (client-side sort/search); archive_date defaults to the last iteration.
Worst Offenders¶
The Worst Offenders tab answers "what should I look at first?" with two fixed-order sections (the search box from SKU Metrics applies here too):
- Stock increased (N) — every (item, site) whose stock grew over the replay (
Δ Stock > 0), sorted by stock increase, largest first. Inventory that built up under the replayed policy is working capital to investigate. - Target missed (N) — every (item, site) with a target where the achieved fill rate fell short (
Diff < 0), sorted by the miss, worst first.
Clicking a row opens the SKU drill-down detail.
SKU Drill-Down Detail¶
Clicking a row in either grid opens a full-screen detail modal for that (item, site):
- Header — item name @ site name.
- Archive navigation — ← Prev, an archive dropdown and Next → let you walk the replay iteration by iteration and watch the situation evolve (the buttons are disabled at the window's edges).
- Inventory balance chart — projected inventory (line), demand (bars), shortage (red line) and safety stock (dashed) over the horizon's week dates: the demand that led to the supply, and the balance that led to backorders.
- Three lists (side by side on wide screens):
- Firm Demand — confirmed future orders from
master.demand_actuals(is_forecast = FALSE) within 365 days of the archive date: date, qty, customer, priority - Recommended Orders — the replay engine's planned orders at that archive: type, release → arrival date, qty, status
- Firm Supply Orders — the inbound order book (
master_supply_orders_firm): type, arrival, qty, ERP status
- Firm Demand — confirmed future orders from
API: GET /api/replay/runs/{id}/series-detail?item_id=&site_id=&archive_date= — archive_date defaults to the last iteration; the response carries prev_archive / next_archive for navigation.
Cancelling and Deleting¶
Cancelling a Running Replay¶
Click Cancel on a running replay. The engine checks for cancellation before each iteration, so the current iteration will complete before the replay stops. Partial results (iterations that already completed) are preserved.
Deleting a Replay Run¶
Click Delete on a completed, failed, or cancelled replay. This removes:
- The replay run metadata
- All iteration records
- The replay pipeline
Deletion is permanent
Deleted replay data cannot be recovered. The source pipeline is never affected by deletion.
Limitations and Performance¶
| Aspect | Detail |
|---|---|
| One replay at a time | Only one replay can run per source pipeline concurrently. Starting a second replay on the same pipeline will fail. |
| Forecast bootstrap | The replay pipeline starts with empty PIPE_* data. On the first iteration the supply step auto-runs the full forecast chain (characterization → backtest → forecast → best-method) for the replay pipeline, then reuses it for every subsequent iteration. The first iteration therefore takes several minutes; later iterations run in seconds. |
| Engine runtime | After the bootstrap, each iteration runs netting + supply + allocation (~10 s on the reference tenant). A 14-week replay ≈ 2–3 min plus one bootstrap. |
| Side-effect-free | Every on-hand delta (receipts, backorder carries) is journaled and reversed when the run ends — success, failure or cancellation. A killed process leaves the journal, which the next run start (or the API startup sweep) reverses automatically. Concurrent on-hand edits made during the run are never clobbered. |
| ClickHouse partitions | Each iteration writes a new archive bucket to PIPE_supply_orders, PIPE_supply_inventory_projection, PIPE_supply_capacity, PIPE_allocation_results and PIPE_allocation_unfulfilled. For daily granularity several iterations share one weekly (Monday) bucket — only the last state per bucket survives. |
| No demand simulation | The replay replays real historical demand (demand actuals + the replay pipeline's own forecast). It does not simulate alternative demand scenarios. |
Worked Example — theBicycle "Default" pipeline, 3-month weekly replay (Sep 2026)¶
The first end-to-end validation of the replay engine was run on the theBicycle tenant (replay run #2, replay pipeline 129):
| Setting | Value |
|---|---|
| Source pipeline | Default (pipeline 4, prod) |
| Window | 2026-06-01 → 2026-08-31 (14 weekly Monday iterations) |
| Demand carry mode | lost_sale |
| Forecast bootstrap | 580 series characterized + backtested on iteration 1 (~6 min) |
Results¶
- All 14 iterations completed; overall fill rate declined 70.1% → 57.1% as summer demand rose, total stock evolved 386k → 351k units, shortage grew 122k → 133k.
- Exactly 6 firm supply orders (1040 units) were received into on-hand across the window — each once, on the iteration covering its arrival week (on_hand evolved 10 688 → 11 728 units during the run, +1 040 byte-exact with the order book). This run predates the side-effect-free journaling introduced later the same day: its receipts were kept. Current engine versions reverse all deltas at run end.
- Every iteration wrote its own CH archive bucket for replay pipeline 129:
14 archive dates across
PIPE_supply_orders(253k rows),PIPE_supply_inventory_projection(541k),PIPE_allocation_results(65k),PIPE_allocation_unfulfilled(26k) andPIPE_forecast_netting(331k).
How to inspect it¶
- UI: theBicycle tenant → Process Runner (
/processes) → 🔄 Replay tab → click the "Default 3-month weekly replay" row. The dashboard shows the run list (completed, iteration 14/14) with three tabs: Overview (the Stock Over Time chart, the Fill Rate & Shortfall Over Time chart and the per-iteration table with metrics), SKU Metrics (743 item-site rows; at the last archive 204 rows missed their segment target) and Worst Offenders (at the last archive: 31 SKUs increased stock, 204 missed target — click any row for the per-iteration drill-down). - API:
GET /api/replay/runs/2/iterations(per-iteration metrics JSON),GET /api/replay/runs/2/stock-evolution,GET /api/replay/runs/2/fill-rate-evolution,GET /api/replay/runs/2/sku-metrics(per-(item, site) target vs. achieved fill + stock start/end),GET /api/replay/runs/2/series-detail?item_id=…&site_id=…(drill-down: timeline, netting, orders, firm demand, firm supply). - ClickHouse (tenant
thebicycle): every replay table is scoped bypipeline_id = 129; group byarchive_dateto walk the timeline, e.g.
SELECT archive_date,
round(sum(shortage)) AS shortage,
round(avg(projected_inventory), 1) AS avg_stock
FROM PIPE_supply_inventory_projection
WHERE pipeline_id = 129
GROUP BY archive_date
ORDER BY archive_date
Rolling back run #2's on-hand receipts (historical)
Run #2 was executed before the side-effect-free journaling landed, so its
receipts were kept in the live on-hand (+1 040 units). A pre-replay snapshot
was kept at master._replay_oh_manual_pre_replay (theBicycle). To restore:
TRUNCATE master.on_hand;
INSERT INTO master.on_hand SELECT * FROM master._replay_oh_manual_pre_replay;
Runs executed after the journaling change need no rollback — they reverse themselves.