WooCommerce Custom Order Status Missing in HPOS Admin

Custom order status registered with register_post_status disappears after WooCommerce HPOS migration. Here is why and the exact filter that brings it back.
WooCommerceHPOSWordPress
June 1, 20265 min read878 words

The Problem

A client's fulfilment workflow depended on three custom order statuses: wc-awaiting-stock, wc-quality-check, and wc-warehouse-hold. Registered with register_post_status() years ago, hooked into status transitions, used by their warehouse pickers every shift. After flipping the switch on High-Performance Order Storage on WooCommerce 10.7, the statuses vanished from the order screen. The status dropdown showed only the seven core statuses. The bulk-actions dropdown showed only the core statuses. Orders that had been sitting in wc-awaiting-stock still rendered with the right label in the list table, but you could not move anything in or out from the UI any longer.

Worse, calls to wc_get_orders([ 'status' => 'awaiting-stock' ]) returned an empty array. Yet the value was still on the order — confirmed via wp wc shell and a direct $order->get_status() call. The data was there. The admin and the lookup APIs were both blind to it.

Why It Happens

register_post_status() registers the status against shop_order as a custom post type. Under HPOS, orders are no longer custom posts. They live in wp_wc_orders. The status column is still populated, but every UI surface (the list table, the dropdowns, the bulk actions, the analytics filter) now reads its set of valid statuses from the WooCommerce-specific filter wc_order_statuses, not from the WordPress post-status registry.

The legacy code path used to read both. The 10.7 admin rewrite for HPOS dropped the post-status fallback. If your status is only registered through register_post_status() it still exists in the global $wp_post_statuses array but nowhere in the list WooCommerce builds for its own UI. The order keeps the value because the database column is dumb, but every layer that decides which values are legal sees an unknown status and either renders the raw slug or hides the order entirely.

The HPOS migration guide mentions that wc_order_statuses is the canonical hook now, but most projects that adopted custom statuses pre-HPOS still have a register_post_status() call sitting in a plugin or theme and never added the WooCommerce filter alongside it.

The Fix

Add the status to wc_order_statuses and, on 10.7 and up, also register it with woocommerce_register_shop_order_post_statuses so the compatibility shim keeps recognising it during partial migrations:

add_filter( 'wc_order_statuses', function ( $statuses ) {
    $statuses['wc-awaiting-stock'] = _x( 'Awaiting Stock', 'Order status', 'my-plugin' );
    $statuses['wc-quality-check']  = _x( 'Quality Check',  'Order status', 'my-plugin' );
    $statuses['wc-warehouse-hold'] = _x( 'Warehouse Hold', 'Order status', 'my-plugin' );
    return $statuses;
} );

add_filter( 'woocommerce_register_shop_order_post_statuses', function ( $statuses ) {
    foreach ( [ 'wc-awaiting-stock', 'wc-quality-check', 'wc-warehouse-hold' ] as $slug ) {
        $statuses[ $slug ] = [
            'label'                     => ucwords( str_replace( [ 'wc-', '-' ], [ '', ' ' ], $slug ) ),
            'public'                    => false,
            'exclude_from_search'       => false,
            'show_in_admin_all_list'    => true,
            'show_in_admin_status_list' => true,
            'label_count'               => _n_noop(
                "{$slug} <span class='count'>(%s)</span>",
                "{$slug} <span class='count'>(%s)</span>",
                'my-plugin'
            ),
        ];
    }
    return $statuses;
} );

The wc_order_statuses filter is what every admin dropdown reads. The woocommerce_register_shop_order_post_statuses filter feeds the post-type compatibility layer that other plugins (reporting, exports, REST API extensions) still hook into. Registering on both keeps the admin and the downstream tooling in sync until WooCommerce fully removes the legacy path.

For wc_get_orders() and the data store lookup APIs, the slug must be the prefix-stripped version, not the wc- form:

$orders = wc_get_orders( [
    'status' => [ 'awaiting-stock', 'quality-check' ],
    'limit'  => -1,
] );

If you pass wc-awaiting-stock you will silently get zero results. The orders controller normalises the slug before querying and the prefixed form does not match. This is the gotcha that costs the most time, because the function neither warns nor errors. It just returns an empty array.

One more thing worth checking after the fix lands. Run the sync verification to make sure HPOS-mode and legacy-mode agree on the status while compatibility mode is still on:

wp wc hpos diff --batch-size=500 --status=any

If any rows show a status mismatch, the row in wp_wc_orders and the row in wp_posts disagree on the status, which usually means the custom status was set while one storage backend was registered and the other was not. Re-run the WooCommerce verify-and-sync tool until the diff comes back empty before you turn compatibility mode off.

The Lesson

Under HPOS, register_post_status() alone is no longer enough for a custom order status. Add the slug to wc_order_statuses for the admin UI, register it on woocommerce_register_shop_order_post_statuses for the legacy compatibility path, and query it through wc_get_orders() using the prefix-stripped slug. Custom workflow statuses are the most common thing that quietly breaks during an HPOS migration, and the fix is roughly six lines of filter glue.

If your HPOS migration left a workflow half-working, that is a project I get paid to finish. See my services. For a related HPOS gotcha, read WooCommerce HPOS order search by email.

Stuck on an HPOS migration that broke a workflow? Get it shipped.

Back to blogStart a project