Create and Manage WooCommerce Variable Products and Variations Programmatically

WooCommerce variable products are products that have multiple purchasable variants — each combination of attribute values (e.g., Size: Large + Color: Red) corresponds to a product variation, which is a child post of type product_variation with its own SKU, price, stock level, and image. Creating a variable product programmatically requires four steps: create the parent product as a WC_Product_Variable object, register the attributes on the product using $product->set_attributes() with WC_Product_Attribute objects, save the product, and then create each variation as a WC_Product_Variation object with the parent ID, the attribute combination, and the pricing data. Product attributes can be global (registered as pa_size taxonomy terms) or local (stored in the product meta as a custom attribute array) — global attributes allow filtering in the shop and share terms across products, while local attributes are simpler for one-off configurations. The WC_Product_Attribute object requires set_id() (taxonomy ID for global, 0 for local), set_name(), set_options() (array of term slugs or option strings), set_position(), set_visible(), and set_variation(true) to mark it as used for variations. Each variation is created by instantiating WC_Product_Variation, setting the parent ID, calling set_attributes() with the specific value combination, setting price, stock, and SKU, then saving. After creating all variations, WC_Product_Variable::sync() must be called with the parent product ID to recalculate the parent’s price range displayed in the shop. Bulk-importing variable products from a spreadsheet or ERP system uses this same programmatic flow in a WP-CLI command or a background task running via Action Scheduler. The custom product fields post shows how to extend this pattern with additional meta fields on both the parent product and individual variations.

Problem: A store migrating from a legacy platform needs to import 500 clothing products, each with up to 12 size/color variation combinations, into WooCommerce — doing it manually through the admin UI would take days and is error-prone.

Solution: Write a function that creates a WC_Product_Variable parent with global attribute terms, then loops over a variation matrix to create each WC_Product_Variation with its specific attribute combination, price, SKU, and stock, and calls WC_Product_Variable::sync() to update the parent price range.

/**
 * Create a variable product with size and color variations.
 *
 * @param array $data {
 *   string $name, string $description,
 *   array  $variations  [ ['size'=>'M','color'=>'Red','price'=>29.99,'sku'=>'SHIRT-M-RED','stock'=>10], ... ]
 * }
 * @return int  Parent product ID
 */
function myplugin_create_variable_product(array $data): int {
    // 1. Create parent variable product
    $product = new WC_Product_Variable();
    $product->set_name(sanitize_text_field($data['name']));
    $product->set_description(wp_kses_post($data['description'] ?? ''));
    $product->set_status('publish');
    $product->set_catalog_visibility('visible');

    // 2. Register global attributes (pa_size and pa_color must exist as taxonomies)
    $size_terms  = array_unique(array_column($data['variations'], 'size'));
    $color_terms = array_unique(array_column($data['variations'], 'color'));

    $size_attr = new WC_Product_Attribute();
    $size_attr->set_id(wc_attribute_taxonomy_id_by_name('pa_size'));
    $size_attr->set_name('pa_size');
    $size_attr->set_options(array_map('sanitize_title', $size_terms));
    $size_attr->set_position(0);
    $size_attr->set_visible(true);
    $size_attr->set_variation(true);

    $color_attr = new WC_Product_Attribute();
    $color_attr->set_id(wc_attribute_taxonomy_id_by_name('pa_color'));
    $color_attr->set_name('pa_color');
    $color_attr->set_options(array_map('sanitize_title', $color_terms));
    $color_attr->set_position(1);
    $color_attr->set_visible(true);
    $color_attr->set_variation(true);

    $product->set_attributes([$size_attr, $color_attr]);
    $parent_id = $product->save();

    // 3. Create each variation
    foreach ($data['variations'] as $v) {
        $variation = new WC_Product_Variation();
        $variation->set_parent_id($parent_id);
        $variation->set_attributes([
            'pa_size'  => sanitize_title($v['size']),
            'pa_color' => sanitize_title($v['color']),
        ]);
        $variation->set_regular_price((string)(float)$v['price']);
        $variation->set_sku(sanitize_text_field($v['sku']));
        $variation->set_stock_quantity(absint($v['stock']));
        $variation->set_manage_stock(true);
        $variation->set_status('publish');
        $variation->save();
    }

    // 4. Sync parent price range and stock status
    WC_Product_Variable::sync($parent_id);

    return $parent_id;
}

// Usage example
$product_id = myplugin_create_variable_product([
    'name'       => 'Classic T-Shirt',
    'description'=> '100% cotton unisex tee.',
    'variations' => [
        ['size' => 'S', 'color' => 'White', 'price' => 24.99, 'sku' => 'TEE-S-WHT', 'stock' => 20],
        ['size' => 'M', 'color' => 'White', 'price' => 24.99, 'sku' => 'TEE-M-WHT', 'stock' => 35],
        ['size' => 'M', 'color' => 'Black', 'price' => 24.99, 'sku' => 'TEE-M-BLK', 'stock' => 28],
        ['size' => 'L', 'color' => 'Black', 'price' => 26.99, 'sku' => 'TEE-L-BLK', 'stock' => 15],
    ],
]);
echo "Created product ID: $product_id";

NOTE: Global attribute terms (pa_size, pa_color) must exist in the database before setting them on a product — use wp_insert_term('Large', 'pa_size') to create missing terms, or use wc_create_attribute() to register the attribute taxonomy itself if it does not exist. Running WC_Product_Variable::sync() after bulk creation is important — skipping it leaves the parent showing an incorrect or empty price range in the shop catalog.