How Synplex Explodes ATO Demand to Components
For assembly-to-order BOMs, demand forecasted against the parent SKU must be distributed down to each component SKU so that replenishment recommendations for components are accurate. This page explains exactly how that explosion works.
The Problem It Solves
Without demand explosion: A component that is only ever used inside a BOM has zero direct sales history. Synplex sees no demand for it and never recommends a reorder — even if 80 units of the parent SKU are forecasted to sell next month. For example, the CPU in a "Custom PC – Base" BOM has 0 direct sales, so without explosion its replenishment recommendation would be none.
With demand explosion: Synplex multiplies the parent SKU forecast by the component's required quantity per unit. If "Custom PC – Base" is forecasted at 80 units/month and the BOM requires CPU × 1, Synplex attributes 80 units/month of demand to the CPU and generates a correct reorder recommendation.
What Triggers a Demand Explosion
The recalculateComponentDemandHistory background action is enqueued for a specific component (identified by its inventoryItemId) whenever:
- A BOM is activated (status: draft → active)
- A BOM is archived (status: active → archived) — zeroes out the component's BOM demand contribution
- A BOM type changes on an active BOM (assemble-to-order ↔ pre-assembled)
- A BOM component is added, updated, or removed
Each enqueue is keyed as component-{inventoryItemId} within the bom-structure-{shopId} queue, so rapid successive changes to the same component are deduplicated — only one recalculation runs.
How the Explosion Works Step by Step
Step 1 — Reverse lookup: find all active parent BOMs
Starting from the component's inventoryItemId, Synplex finds every active BOM that contains this component and records the required quantity per unit for each.
Example — Component: RAM 16GB (inventoryItemId: 987)
| BOM name | Required qty per unit |
|---|---|
| "Custom PC – Base" | RAM × 2 |
| "Workstation Bundle" | RAM × 4 |
| If no active BOMs are found, the action exits immediately — nothing to calculate. |
Step 2 — Fetch all future demand plans for the parent SKUs
Synplex loads every demandPlan record for all parent variant IDs where monthFirstDay is today or later, filtered to included locations only.
| Parent SKU | Location | Month | Units |
|---|---|---|---|
| "Custom PC – Base" | London | Aug 2025 | 80 |
| "Custom PC – Base" | London | Sep 2025 | 95 |
| "Workstation Bundle" | London | Aug 2025 | 20 |
| "Workstation Bundle" | London | Sep 2025 | 25 |
| Pagination is handled automatically — all pages are fetched before proceeding. |
Step 3 — In-memory matrix aggregation
For each parent demand plan, Synplex multiplies the planned sales quantity by the component's required quantity for that BOM, then accumulates the result per (locationId, monthFirstDay) pair.
Contributions for RAM 16GB:
- "Custom PC – Base" × 2 per unit: London Aug 2025 = 80 × 2 = 160 / London Sep 2025 = 95 × 2 = 190
- "Workstation Bundle" × 4 per unit: London Aug 2025 = 20 × 4 = 80 / London Sep 2025 = 25 × 4 = 100
Resulting demand matrix for RAM 16GB:
| Location | Month | Aggregated demand |
|---|---|---|
| London | Aug 2025 | 160 + 80 = 240 units |
| London | Sep 2025 | 190 + 100 = 290 units |
Step 4 — Fetch existing component demand plans
Synplex loads the existing demandPlan records that already have a plannedBOMQuantity for this component, so it can diff against them before writing.
Step 5 — Upsert with change detection
For each entry in the demand matrix, Synplex checks whether the existing plannedBOMQuantity differs from the newly calculated value by more than 1% (the change threshold). If the change is insignificant, the write is skipped to avoid unnecessary database load.
Example — change below threshold, write skipped:
- Existing
plannedBOMQuantityfor London Aug 2025: 238 - New calculated value: 240
- Change: |240 − 238| ÷ 238 = 0.84% → below 1% threshold
- Result: write skipped
Example — change above threshold, record upserted:
- Existing
plannedBOMQuantityfor London Sep 2025: 180 - New calculated value: 290
- Change: |290 − 180| ÷ 180 = 61% → above threshold
- Result:
demandPlanupserted withplannedBOMQuantity= 290
Records are upserted on their recordKey field, composed as:
{shopId}-{componentVariantId}-{inventoryLevelId}-{YYYY-MM}
This ensures exactly one plannedBOMQuantity row exists per component × location × month.
Step 6 — Orphan cleanup
If a location or month that previously had BOM demand no longer appears in the new demand matrix (e.g. a location was excluded, or a parent BOM was archived), its plannedBOMQuantity is set to 0 rather than deleted. This preserves the plan row for historical reference while removing its effect on replenishment calculations.
How Component Demand Plans Feed Replenishment
The plannedBOMQuantity field on a demandPlan record is combined with
the component's own direct plannedSalesQuantity when Synplex runs the
inventory simulation for that component.
Example — RAM 16GB · London · August 2025:
plannedSalesQuantity: 0 (RAM is never sold standalone)plannedBOMQuantity: 240 (exploded from parent BOMs)- Total demand used in simulation: 240 units
refreshVariantMetrics uses this total demand to calculate:
reorderDatecoverageDaterecommendedOrderQtystockAssessment(healthy / running low / out of stock / overstocked)
The component appears in the demand plan, inventory report, and supply plan with accurate figures — as if it had 240 units of its own direct demand — even though it is never sold standalone.
What Happens When an ATO BOM Is Archived
When a BOM is archived, recalculateComponentDemandHistory is enqueued for every component with triggeredBy: "status-archived". The action runs through the same steps above but finds no active BOMs for the component, so the demand matrix is empty. All existing plannedBOMQuantity values are then zeroed out.
The cascade on archive:
recalculateComponentDemandHistoryenqueued per component- No active BOMs found in Step 1
- Demand matrix is empty
- All
plannedBOMQuantityrows set to 0 — component replenishment recommendations revert to direct sales demand only
Troubleshooting
Component shows no BOM demand despite active ATO BOM
- BOM status is "active" — demand explosion only runs for active BOMs
- BOM type is "assemble-to-order" — pre-assembled BOMs use a different demand attribution path
- Parent SKU has demand plans with
plannedSalesQuantity> 0 — if the parent has no forecast, there is nothing to explode - Component's
inventoryItemIdis linked on itsBomComponentrecord — missing links are visible in the BOM detail and result in no quantity being attributed - The location is marked as "included" in location settings
BOM demand seems too high or too low
- Component quantity on the BOM record is correct (e.g. RAM × 2 per unit, not × 1)
- All parent BOMs using this component are accounted for — a component shared across multiple BOMs accumulates demand from all of them
- Parent demand plans are up to date — trigger a forecast refresh if the parent SKU's plan looks stale
BOM demand updated correctly but reorder date did not change
The demand explosion writes to plannedBOMQuantity on demandPlan records. The reorderDate is computed separately by refreshVariantMetrics, which reads those demand plans as input to the inventory simulation.
If the reorderDate has not updated, refreshVariantMetrics has not yet run for this component since the demand explosion completed. It runs on a schedule and also when inventory levels change. Wait for the next scheduled run or trigger a manual variant metrics refresh from the variant detail page.