Add a Custom WooCommerce Product Status with register_post_status and WC Filters

WooCommerce products use WordPress’s post status system under the hood — the standard publish, draft, pending, and private statuses all work for products. WooCommerce adds its own wc-active, wc-suspended, and similar order statuses on top. When you need a custom product status beyond the defaults — “Out of Season”, “Coming Soon”, “Discontinued”, or “Requires Approval” — you must integrate at both the WordPress post status level and the WooCommerce layer: register the status with register_post_status(), add it to the WooCommerce product status list, ensure it appears in the product editor’s status dropdown, and optionally apply a colour badge in the admin product list. All four steps are required; omitting any one of them results in the status being saved correctly but not displayed or selectable in the editor UI.

Problem: You need a "Coming Soon" product status that appears in the WooCommerce product editor's status dropdown, shows a coloured badge in the admin product list, and hides the product from shop listings while still allowing direct URL access.

Solution: Register the post status with register_post_status(), add it to WooCommerce product status arrays via filters, inject it into the quick-edit and bulk-edit dropdowns, and apply a CSS badge colour via admin_head.

<?php
add_action( 'init', 'register_coming_soon_product_status' );

function register_coming_soon_product_status() {
    register_post_status( 'coming-soon', [
        'label'                     => _x( 'Coming Soon', 'WooCommerce product status', 'textdomain' ),
        'public'                    => true,   // accessible at direct URL
        'exclude_from_search'       => true,   // hidden from search
        'show_in_admin_all_list'    => true,
        'show_in_admin_status_list' => true,
        'label_count'               => _n_noop( 'Coming Soon <span class="count">(%s)</span>',
                                                'Coming Soon <span class="count">(%s)</span>',
                                                'textdomain' ),
    ] );
}

// ── Add to WooCommerce product status list ─────────────────────────────
add_filter( 'wc_product_statuses', 'add_coming_soon_to_product_statuses' );

function add_coming_soon_to_product_statuses( $statuses ) {
    $statuses['coming-soon'] = __( 'Coming Soon', 'textdomain' );
    return $statuses;
}

// ── Show in the product editor's "Product Status" dropdown ─────────────
add_action( 'admin_footer-post.php',       'add_coming_soon_to_editor_dropdown' );
add_action( 'admin_footer-post-new.php',   'add_coming_soon_to_editor_dropdown' );

function add_coming_soon_to_editor_dropdown() {
    global $post;
    if ( isset( $post ) && 'product' === $post->post_type ) {
        $selected = ( 'coming-soon' === $post->post_status ) ? 'selected="selected"' : '';
        echo '<script>
            jQuery(function($){
                $("select#post_status").append(
                    "<option value='coming-soon' ' . $selected . '>'
                    . esc_js( __( "Coming Soon", "textdomain" ) )
                    . '</option>"
                );
                ' . ( $selected ? '$(".misc-pub-post-status #post-status-display").text("' . esc_js( __( "Coming Soon", "textdomain" ) ) . '");' : '' ) . '
            });
        </script>';
    }
}

// ── Hide from main shop loop (but accessible via direct URL) ───────────
add_action( 'pre_get_posts', 'hide_coming_soon_from_shop' );

function hide_coming_soon_from_shop( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) return;
    if ( $query->is_post_type_archive( 'product' ) || $query->is_tax( 'product_cat' ) ) {
        $current_statuses = (array) $query->get( 'post_status' );
        $query->set( 'post_status', array_diff( $current_statuses, [ 'coming-soon' ] ) );
    }
}

// ── Coloured badge in admin product list ──────────────────────────────
add_action( 'admin_head', function () {
    echo '<style>
        .order-status.status-coming-soon { background: #d1ecf1; color: #0c5460; }
        #post-status-info .status-coming-soon { color: #0c5460; }
    </style>';
} );

NOTE: WooCommerce 3.0+ also requires you to add the status to the woocommerce_valid_order_statuses_for_payment and woocommerce_valid_order_statuses_for_cancel filters if the product status should influence order processing. For products only (not orders), the filters shown above are sufficient. Always test the status with get_post_status( $product_id ) after saving to confirm WordPress is persisting the value correctly.