BOM execution end-to-end
This guide walks through a single order from start to finish: a customer places an order containing a BOM-bound variant; Assemblified processes it; a refund happens; the cascade rule plays out. The goal is to give you a mental model of what’s actually happening so you can predict (and debug) any specific case.
On this page
Section titled “On this page”- The setup
- Order placed: what fires
- Pre-assembled drawdown
- Component decrement and Shopify sync
- Dynamic adjustment cascade
- The execution log
- A refund happens
- What a
Keep Assembled = ONrefund looks like
The setup
Section titled “The setup”You sell Vanilla Candle 8oz. It’s a Shopify product with one variant. You have a BOM bound to that variant:
- Direct components:
- 1 × Glass jar (Shopify-linked raw material)
- 1 × Vanilla scent oil (virtual material)
- Sub-assembly: 1 × “Wick assembly”, which contains:
- 1 × Raw wick (Shopify-linked, 8% waste)
- 0.5 × Wick clip (virtual)
Settings:
- Status: Active.
- Pre-assembled quantity: 5 (you built ahead of time).
- Dynamic adjustment: On.
- Keep Assembled on Return: Off.
- “Wick assembly” pre-assembled quantity: 3.
- “Wick assembly” Keep Assembled on Return: Off.
Shopify variant inventory is currently displayed as 45.
Order placed: what fires
Section titled “Order placed: what fires”A customer orders 8 units of Vanilla Candle 8oz.
-
Shopify fires the
ORDERS_UPDATEDwebhook to Assemblified. -
Assemblified receives the webhook. It validates the signature and dedupes on the Shopify event ID.
-
The disambiguator inspects the payload —
cancelled_atis null and there are no refunds. This is a CREATE. -
The work is enqueued. A background job buffers the order so it can be retried if anything fails.
-
Assemblified processes the order. It opens an execution log row, fetches the order’s line items, and matches against active BOMs.
The pipeline up to this point is webhook plumbing — fast, no inventory changes yet.
Pre-assembled drawdown
Section titled “Pre-assembled drawdown”The handler walks the BOM’s recipe with order quantity = 8.
-
BOM-level pre-assembled. Available = 5. Consume
min(5, 8) = 5. Shelf → 0. Remainder = 3. -
Recurse into “Wick assembly” with multiplier = 3 (since the parent ordered 3 more after the shelf was exhausted; the SA reference qty is 1).
-
“Wick assembly” pre-assembled. Available = 3. Consume
min(3, 3) = 3. Shelf → 0. Remainder = 0. -
Recursion stops because the “Wick assembly” remainder is zero — its components don’t get touched.
-
Direct components for the BOM-level remainder. Wait — but the BOM-level remainder was 3, and the SA shelf covered all of it. Glass jar and Vanilla oil are direct components of the BOM, not the SA. They’re emitted at the BOM level for the 3 units that didn’t come off the BOM’s own shelf:
- Glass jar × 3 (qty 1 per unit).
- Vanilla oil × 3.
Net consumption:
- BOM pre-assembled: -5.
- Wick assembly pre-assembled: -3.
- Glass jar: -3.
- Vanilla oil: -3.
- Raw wick: 0 (covered by Wick assembly shelf).
- Wick clip: 0.
Component decrement and Shopify sync
Section titled “Component decrement and Shopify sync”Assemblified builds two lists of changes:
- Shopify-linked changes: Glass jar -3 at the default location.
- Virtual changes: Vanilla oil -3.
The Shopify list is sent in one inventory-adjustment call. The virtual list is applied to Assemblified’s internal inventory.
The execution log records the calculated changes, the sync timestamps, and the response status from Shopify.
Dynamic adjustment cascade
Section titled “Dynamic adjustment cascade”Because the BOM has dynamic adjustment on, after the consumption, Assemblified recomputes the BOM’s displayed Shopify quantity:
- Glass jar: was 90, now 87. Buildable from this: 87.
- Vanilla oil: was 100, now 97. Buildable: 97.
- Wick assembly’s components (raw wick, wick clip) — unchanged. But the SA shelf is now 0; raw wick available 50 ÷ 1.08 = 46 buildable, wick clip 100 ÷ 0.5 = 200 buildable.
- Bottleneck: min(87, 97, 46, 200) = 46.
- Plus pre-assembled: BOM shelf 0 + “Wick assembly” shelf 0 = 0 toward the BOM (only the BOM’s own shelf counts at this layer; the SA shelf is consumed inside).
Wait — the math here is simpler than the spec. The dynamic adjustment looks at the BOM’s own buildable count from current component availability. Pre-assembled at the BOM level adds; the SA’s pre-assembled is part of the recursive expansion.
Let’s revisit. After the order:
- BOM pre-assembled: 0.
- Direct components (jar, oil): the bottleneck calc walks the BOM tree. At the SA layer, it considers pre-assembled — currently 0. Then nests into raws. So the buildable count is
min(jar, oil, raw_wick / 1.08, wick_clip / 0.5).
Plug in: min(87, 97, 46, 200) = 46.
The new displayed quantity is 46 + 0 = 46.
The previous displayed was 45 — wait, that doesn’t reconcile. The reason: dynamic adjustment runs after the order completes, and the previous “45” was the result of the previous recompute. The maintain-same-level setting is off, so Shopify decremented by 8 from 45 to 37 during the order. Now the dynamic adjustment pushes it to 46 — a delta of +9.
This kind of math is where the audit log is invaluable.
The execution log
Section titled “The execution log”A new row in bom_execution_log captures everything:
- Timestamps for webhook receipt, queue lifecycle, processing, and Shopify sync.
bomsProcessedincludes the Vanilla Candle BOM.bomProcessingMetadataincludes the per-BOM execution result with material requirements.calculatedChangesincludes the Shopify-side deltas (jar -3).preAssembledConsumptionincludes:- BOM-level: -5 (entityType: bom).
- SA-level: -3 (entityType: subassembly).
dynamicBomAdjustmentMeatadata(yes, that misspelling is intentional in schema) includes the recompute result.
Operators see this row in the BOM detail page’s execution log section.
A refund happens
Section titled “A refund happens”Two days later, the customer refunds 2 units.
- Shopify fires
ORDERS_UPDATEDwith refund line items. - The disambiguator sees the refund payload → operation = REFUND.
- Assemblified computes restoration for 2 units.
- Cascade rule applies. Parent BOM Keep Assembled = OFF, “Wick assembly” Keep Assembled = OFF — so all raws return.
- Material requirements computed by
calculateMaterialRequirementsForReturn(bom, 2):- Glass jar +2.
- Vanilla oil +2.
- Raw wick + (2 × 1 × 1.08) = +2.16.
- Wick clip + (2 × 0.5) = +1.
- Pre-assembled restoration runs
restorePreassembledQuantitiesOnReturn(bom, 2, locationId). Since parent flag is off, it recurses. “Wick assembly” flag is off too, so no SA-level restoration either. - Shopify sync writes positive deltas. Virtual updates write to
virtual_inventory_levels. - Logistified gets the full demand decrease regardless of cascade — that’s the asymmetry.
The refund log row is written. refundHistory[] gets a new entry. If a full cancel comes later, the netting logic will subtract this 2 from the cancel quantity.
What a Keep Assembled = ON refund looks like
Section titled “What a Keep Assembled = ON refund looks like”Suppose instead the BOM had Keep Assembled = ON. The refund of 2 would:
- Cascade halt at the BOM level.
- BOM pre-assembled gets +2. Shelf → 2.
- Nothing else changes. Glass jar, oil, raw wick, wick clip — all unchanged. Wick assembly pre-assembled — unchanged.
- Logistified still gets the full demand decrease.
This is useful for finished goods that physically can’t be disassembled. The unit goes back to your finished-goods shelf, ready to be re-sold.
Where to next
Section titled “Where to next”- BOM execution model — the reference for the pipeline.
- Pre-assembled inventory — drawdown semantics.
- Refunds & cancellations — the cascade rule with worked examples for every branch.