WooCommerce Custom Order Status: Register, Style, and Trigger Email on Status Transition

WooCommerce orders move through a lifecycle of statuses: pending → processing → completed (or cancelled, refunded, failed, on-hold). For most stores these built-in statuses are sufficient, but complex fulfilment workflows often need custom statuses — “awaiting supplier”, “partially shipped”, “ready for pickup”, “quality check”, and so on. WooCommerce stores order statuses as custom WordPress post statuses (the shop_order CPT), and adding a custom status requires: (1) registering it with register_post_status(), (2) telling WooCommerce to include it via the wc_order_statuses filter, (3) adding a colour and icon in the WooCommerce admin UI via CSS, and optionally (4) controlling which status transitions are allowed via the woocommerce_valid_order_statuses_for_* filters. Getting any step wrong results in statuses that appear in the dropdown but are invisible in the list, or that trigger the wrong email templates.

Problem: A furniture store needs a "Ready for Pickup" status that appears in the order admin list with a distinct colour, allows the store to email the customer using a custom email template, and allows the order to be moved to "Completed" afterwards.

Solution: Register the status with register_post_status(), add it to WooCommerce's list, style it with inline CSS, and register a custom order email class triggered by the status transition.

<?php
// ── 1. Register post status (must run early, before WC reads statuses) ─
add_action( 'init', function () {
    register_post_status( 'wc-ready-pickup', [
        'label'                     => _x( 'Ready for Pickup', 'Order status', 'textdomain' ),
        'public'                    => true,
        'show_in_admin_all_list'    => true,
        'show_in_admin_status_list' => true,
        /* translators: %s = number of orders */
        'label_count'               => _n_noop( 'Ready for Pickup (%s)', 'Ready for Pickup (%s)', 'textdomain' ),
        'exclude_from_search'       => false,
    ] );
} );

// ── 2. Add to WooCommerce status list ─────────────────────────────────
add_filter( 'wc_order_statuses', function ( array $statuses ): array {
    // Insert after 'wc-processing' in the list
    $new = [];
    foreach ( $statuses as $key => $label ) {
        $new[ $key ] = $label;
        if ( 'wc-processing' === $key ) {
            $new['wc-ready-pickup'] = __( 'Ready for Pickup', 'textdomain' );
        }
    }
    return $new;
} );

// ── 3. Style the status badge in admin ────────────────────────────────
add_action( 'admin_head', function () {
    echo '<style>
    .order-status.status-ready-pickup {
        background: #e8f5e9;
        color: #2e7d32;
    }
    </style>';
} );

// ── 4. Allow transition to/from the new status ────────────────────────
// Allow moving to completed from ready-pickup
add_filter( 'woocommerce_valid_order_statuses_for_payment_complete', function ( array $statuses ): array {
    $statuses[] = 'ready-pickup'; // without 'wc-' prefix
    return $statuses;
} );

// ── 5. Send email when order moves to ready-pickup ────────────────────
// The action woocommerce_order_status_{from}_to_{to} fires on every transition
add_action( 'woocommerce_order_status_processing_to_ready-pickup', function ( int $order_id ) {
    $order = wc_get_order( $order_id );
    if ( ! $order ) return;

    $customer_email = $order->get_billing_email();
    $subject = sprintf( __( 'Your order #%s is ready for pickup!', 'textdomain' ), $order->get_order_number() );
    $message = sprintf(
        __( 'Hello %s, your order is ready to collect from our store.', 'textdomain' ),
        esc_html( $order->get_billing_first_name() )
    );

    wc_mail( $customer_email, $subject, $message );
} );

NOTE: WooCommerce internally strips the wc- prefix when storing the status in order functions — $order->get_status() returns 'ready-pickup', not 'wc-ready-pickup'. Use the prefix-less version in PHP comparisons and the transition hooks. The register_post_status() call must run on init, not later — WooCommerce reads statuses during its own initialisation. To include the custom status in WooCommerce reports (the analytics dashboard), add it to the woocommerce_reports_order_statuses filter. Custom order statuses do not automatically get dedicated email templates — you need to register a WC_Email subclass or use the simpler wc_mail() helper shown above for basic notifications.