Skip to main content

Kit & bundle component reference

This page is a field-by-field lookup for how a kit or bundle stores its components, what the Workshop returns when you open a bundle or kit, how the Buildable count is derived from component stock, and how the Total unit cost rolls up. It's reference material — for the task of assembling a bundle or kit, see Build bundles & kits in the Workshop. For the difference between the two types, see Kits vs bundles and Product types explained.

The product_components pivot

Components are stored as rows in the product_components pivot table. Each row links a parent bundle/kit to one component product and records how many of that component the parent needs.

FieldTypeMeaning
idintegerPrimary key of the pivot row. Exposed to the UI so a specific component line can be addressed.
parent_product_idintegerThe bundle or kit that contains the component.
component_product_idintegerThe product used as the component.
quantityfloatHow many of the component are needed per one parent. Cast to float, but the API and UI enforce a minimum of 1 (see Save payload).
created_at / updated_attimestampRow lifecycle timestamps.

A single component product can appear in many parents, and a parent can hold many components — the pivot is many-to-many in both directions.

How a product reaches its components

The Product model exposes the relationship from both ends. Knowing which direction you are reading matters when you interpret a component's stock or when you ask "what breaks if I delete this?".

RelationshipDirectionReturns
componentsparent → childrenThe products this bundle/kit is made of. Carries the pivot id and quantity.
bundles / parentComponentschild → parentsThe bundles/kits that contain this product as a component. Use this to answer "what am I part of?".

Each component in components carries its pivot, so you read the per-parent quantity as component.pivot.quantity.

A pivot row can also carry an assembly InventoryMovement through its polymorphic inventoryMovement link — this is how a kit assembly ties back to the component draw-down in the inventory ledger. For how those movements post to stock and FIFO layers, see The movement ledger in the Inventory guide.

Workshop component payload & display fields

When you open a bundle or kit in the Workshop, the server returns each component as a flat object. These are the exact fields you see per component row.

FieldSourceNotes
idcomponent product idIdentifies the component product (not the pivot row). This is the id you send back when saving.
skucomponent productThe component's SKU.
namecomponent productThe component's name.
quantitypivot quantityQuantity of this component per one parent, cast to a number.
typecomponent productThe component's product type (for example standard or kit for a nested kit).
available_stockcomponent stock cacheAll-warehouse available stock for the component. Falls back to 0 when the component has no cached inventory row.
unit_costcomponent productThe component's unit cost. Falls back to 0 when the component's unit_cost is null.

The response wraps these components with two computed totals:

FieldMeaning
componentsThe array of component objects above.
buildable_quantityHow many complete parents you could assemble from current component stock — see Buildable quantity.
total_unit_costThe summed component cost of one parent, rounded to 2 decimals — see Total unit cost.
available_stock vs a real stock figure

available_stock on a component row is a display-only snapshot from the stock cache. It's not a per-warehouse figure and it's not the same number that gates assembly. The buildable count (below) is computed separately and per-warehouse. For authoritative stock numbers, on-hand vs available, and how each state is derived, see Stock states in the Inventory guide.

Save payload & validation

Saving components replaces the parent's component set. The save payload is deliberately narrow — it accepts only id and quantity per component; every other field in the display payload (sku, name, type, available_stock, unit_cost) is read-only and ignored on save.

{
"components": [
{ "id": 1024, "quantity": 2 },
{ "id": 1050, "quantity": 1 }
]
}
RuleConstraint
componentsRequired, array, at least 1 entry. Saving an empty set is rejected — a bundle or kit must have at least one component.
components.*.idRequired, integer, must exist in products.
components.*.quantityRequired, numeric, minimum 1.

If validation fails the request returns 422 with per-field messages (for example, "At least one component is required." or "Component quantity must be at least 1."). A kit's components stay editable regardless of its inventory history — the only rejections are the configuration rules above (empty set, invalid component type, self-reference, circular reference). For that flow and its guardrails, see Build bundles & kits in the Workshop and Convert & auto-detect product types.

Component eligibility rules

Beyond the field-level rules above, the save also enforces which products are allowed to be components. A payload that passes field validation can still be rejected with a 422 if it breaks one of these rules. The same checks run whether you save from the Workshop or add components while converting a standard product to a bundle/kit.

RuleBehavior
Allowed component typesA component must be a standard or kit product. A bundle, matrix, blemished or manufactured product can't be added as a component — the save is rejected with a message naming the offending SKU.
No self-referenceA product can't be a component of itself. Adding the parent's own id to its component list is rejected.
No circular referenceAdding a kit that already contains this parent (directly or through its own nested components) is rejected, because it would create an assembly loop.
Quantity floor (second guard)Quantity is re-checked as it's written: anything below 1 is rejected even if it somehow bypassed request validation.
Set replacementComponents present on the parent but absent from the new payload are removed. The payload is the full desired set, not a delta.

Buildable quantity

Buildable quantity answers: given current component stock, how many complete parents could you assemble right now? It's computed for both bundles and kits in the Workshop.

How it's computed

The parent's components are recursively flattened — a nested kit is expanded down to its base standard components, and quantities are multiplied through each level of the hierarchy. Availability is then evaluated per warehouse from the stock cache, and the buildable-from-components count for each warehouse is summed to give the total buildable across the business.

Within a single warehouse, the per-component buildable is floor(available ÷ required-per-parent), and the limiting (bottleneck) component — the one with the smallest per-component buildable — sets the warehouse's buildable count.

BehaviorDetail
Per-component mathfloor(available ÷ required-per-parent). The bottleneck component caps the count.
Nested kitsA sub-kit is flattened to its base components; a kit inside a kit doesn't contribute its own on-hand as if it were a raw part.
Quantity multiplicationQuantities compound through the hierarchy. Kit A holds 2× Kit B, Kit B holds 3× Component X → the flattened requirement is 6× Component X per one Kit A.
Per-warehouse, then summedA parent can only be assembled from stock co-located in one warehouse. Buildable is computed per warehouse and the per-warehouse counts are added together.
No componentsA parent with no components is 0 buildable.
Circular referencesThe flattening walk tracks visited products, so a component that (mis)references its own parent can't cause an infinite loop.

Because availability comes from the stock cache, the Workshop's buildable number is a fast display figure. The authoritative, lock-taking check that gates a real assembly reads live on-hand at the moment of assembly, so the two can differ briefly if stock has just moved. For how component draw-downs and the finished kit post to stock, see The movement ledger in the Inventory guide.

Total unit cost

Total unit cost is the summed cost of one parent's components:

total_unit_cost = Σ (component.unit_cost × component.quantity) (rounded to 2 dp)
BehaviorDetail
FormulaSum of each component's unit_cost times its per-parent quantity, rounded to 2 decimal places.
Null costsA component with a null unit_cost contributes 0 to the roll-up.
Nested kitsA nested kit contributes its own stored unit_cost, not a freshly recomputed roll-up of the sub-kit's components.
Stored, not live COGSThe roll-up uses each component's stored unit_cost as-is. This can be stale relative to actual FIFO cost of goods sold — it's an estimate of build cost, not a posted COGS figure.

For how a bundle's revenue is split across its components at sale time (a separate concern from build cost), see Bundle pricing & revenue proration. For actual FIFO cost of goods sold, see FIFO layers & COGS in the Inventory guide.

Before you begin

  • Working with components requires access to the Products area and the Bundle Workshop. See Product permissions reference.
  • Only bundle and kit products hold components. Standard, matrix, blemished and manufactured products don't appear in the Workshop. See Product types explained.

Next steps

Last verified: