Product catalog modeling¶
product-catalog-modeling
Model a sellable catalog of simple products, variants, attributes, SKUs, and stock, and check the model before importing it. Use when designing product data, variation matrices, inventory rules, or catalog imports.
Version 0.3.2 · MIT · Source · Rule
Install this skill
Copy the skill and its rule into your project:
The skill goes to .agents/skills/ and the rule to .agents/rules/. See Command line for other directories.
Use this skill before writing import code or a product schema. The document checked by scripts/validate-catalog.mjs is a planning model. It is not a WooCommerce export. Map it to WooCommerce with the variation notes. An example that passes the checker is assets/example-catalog.json.
Procedure¶
- Decide the purchasable unit. A simple product is purchased as itself. A variable product is purchased as one variant. A grouped product is a collection of other products. An external product is sold somewhere else.
- Give every purchasable unit one SKU. SKUs are unique across the catalog, including variants.
- Model variation attributes as a fixed set of options. Every variant picks exactly one option for every variation attribute. Two variants cannot share a combination.
- Put the price and the stock record on the purchasable unit. A variable parent does not have its own price or SKU in this model.
- Run
node scripts/validate-catalog.mjs --file catalog.jsonfrom this skill directory and fix every reported error before importing.
Document shape¶
{
"products": [
{
"id": "shirt",
"type": "variable",
"sku": null,
"name": "Cotton shirt",
"attributes": [
{ "code": "color", "name": "Color", "options": ["blue", "red"], "variation": true }
],
"variants": [
{
"id": "shirt-blue",
"sku": "SHIRT-BLUE",
"attributes": { "color": "blue" },
"price": "19.00",
"currency": "USD",
"stock": { "managed": true, "quantity": 4, "backorders": "no", "status": "instock" }
}
]
}
]
}
Rules the checker enforces:
typeissimple,variable,external, orgrouped.priceis a major-unit string with two decimal places, such as"19.00".currencyis an ISO 4217 code. Map to the store's decimal setting at the boundary. Do not send this string through a binary float.- Simple and external products require
sku,price, andcurrency. External products also requireproductUrl. - Variable products require
sku: null, no parentprice, at least one attribute with"variation": true, and at least one variant. Each variant sets exactly those attributes. - Grouped products list
childrenids and do not carry a price. stock.managed: truerequires an integerquantityof zero or more.stock.managed: falserequiresquantity: null.backordersisno,notify, oryes.- Optional
stock.statusisinstock,outofstock, oronbackorder. When quantity is managed, status should agree with quantity and backorders. The checker does not infer status for you.
Inventory rules¶
- Managed stock is a quantity you decrement on paid orders and restore on cancel or refund, using the platform's stock API once.
- Unmanaged stock is a status you set by hand (
instockoroutofstock). Do not also store a quantity. - Backorders mean you accept orders at zero.
notifyshows the shopper that the item is on backorder.yesallows it quietly.nostops the sale at zero. - A variable parent's availability is the rollup of its variants. Do not maintain a second parent quantity that can disagree with the variants.
- Reservation during checkout, when the platform holds stock for unpaid orders, is part of available-to-sell. Do not subtract that hold a second time in your importer.
Attributes¶
- A variation attribute changes the purchasable unit (size, color). It belongs in
attributeswithvariation: trueand on each variant. - A descriptive attribute (material, care) is not a variant axis. Store it with
variation: falseor as content. Do not create a variant per descriptive value. - Keep option values stable. Translate the display label, not the option code. The search skill depends on stable facet ids.
Gotchas¶
- A parent price range is derived from variants. Writing a parent price and different variant prices hides which amount will be charged.
- Duplicate SKUs across a simple product and a variant make stock and fulfillment ambiguous.
- Cartesian explosions are real. Three attributes with ten options are a thousand variants. Split the product or drop an axis before generating them.
- Currency is a property of the price, not of the shopper's browser. A multi-currency display converts from the stored currency. It does not create a second SKU.
Verification¶
- The checker exits 0 on the catalog file.
- Every SKU resolves to one purchasable unit.
- A variant combination that is not for sale is absent, not stored with a blank price.
- After import, the storefront price and stock for one variant match that variant's record, not the parent.
Evaluation¶
Worked examples and the checks they must satisfy are in evals/tasks.json.
References¶
Mapping the catalog model to WooCommerce¶
WooCommerce product types match this model: simple, variable, external, and grouped.
Global attributes¶
Create a global attribute once, then attach terms. The taxonomy name is pa_ plus the attribute slug. Variation attribute values stored on a WC_Product_Variation are term slugs, not translated labels.
$product = new WC_Product_Variable();
$product->set_name( 'Cotton shirt' );
$product->set_status( 'publish' );
$attribute = new WC_Product_Attribute();
$attribute->set_id( wc_attribute_taxonomy_id_by_name( 'pa_color' ) );
$attribute->set_name( 'pa_color' );
$attribute->set_options( array( $blue_term_id, $red_term_id ) );
$attribute->set_visible( true );
$attribute->set_variation( true );
$product->set_attributes( array( $attribute ) );
$product->save();
$variation = new WC_Product_Variation();
$variation->set_parent_id( $product->get_id() );
$variation->set_attributes( array( 'pa_color' => 'blue' ) );
$variation->set_sku( 'SHIRT-BLUE' );
$variation->set_regular_price( '19.00' );
$variation->set_manage_stock( true );
$variation->set_stock_quantity( 4 );
$variation->set_backorders( 'no' );
$variation->save();
WC_Product_Variable::sync( $product->get_id() );
Call WC_Product_Variable::sync() after variant writes so the parent price range and stock status match the children. Skip it and the catalog can show the parent as out of stock while a variant has quantity.
Local attributes (not a pa_ taxonomy) are valid for a single product. Prefer global attributes when the same facet must be shared across products and search.
Simple products¶
$product = new WC_Product_Simple();
$product->set_name( 'Gift card' );
$product->set_sku( 'GIFT-25' );
$product->set_regular_price( '25.00' );
$product->set_manage_stock( false );
$product->set_stock_status( 'instock' );
$product->save();
set_regular_price() takes a decimal string. Sale price is set_sale_price(). The active price follows WooCommerce sale schedule rules. Do not overwrite _price meta by hand.
SKU lookup¶
wc_get_product_id_by_sku() resolves both parent products and variations. Uniqueness is enforced unless wc_product_has_unique_sku is filtered. Do not filter it to allow duplicates unless the shop has a separate identity for the sellable unit.
Stock¶
set_manage_stock( true )plusset_stock_quantity()is managed stock.set_stock_status()is the manual status used when stock is not managed, and the cached status when it is.- Reducing stock for an order is
wc_reduce_stock_levels()/wc_maybe_reduce_stock_levels(), which no-ops when_order_stock_reducedis already set. Call it from payment completion only if core has not already done so. woocommerce_hold_stock_minutes(default 60) holds stock on unpaid pending orders. Importers and availability displays should use WooCommerce's availability helpers rather than subtractingstock_quantitya second time.
Grouped and external¶
Grouped products store child ids. The price is the child's price. External products store a product URL and an optional button label. They are not stock-managed inside the shop.
Imports¶
Resolve SKU to id before update. Create when absent. Wrap a large import in batches and call WC_Product_Variable::sync() per parent after its variants, not once for the whole catalog at the start. Delete a variant that left the model. Leaving it published sells a combination the model no longer allows.
Evaluation¶
Fixtures from evals/tasks.json. See evaluation fixtures for how the checks work.
create-variation¶
Create a WooCommerce variation with its own SKU and price under a variable parent that has no SKU.
Must include: WC_Product_Variation, set_sku
Must not include: update_post_meta
Worked example
Create the purchasable variation and give it the SKU.
sync-parent¶
After writing variations, refresh the parent price range and stock status from the children.
Must include: WC_Product_Variable::sync
Must not include: set_sku( 'PARENT' )