Add a Custom Order Status in WooCommerce
In this tutorial, we’re going to dive into WooCommerce custom order statuses.
In the first part of the tutorial, I will show you how to add a new order status in WooCommerce; in the second part, we’ll add it to WordPress bulk actions as well, so you can change order statuses for multiple orders at the same time.
One more thing: we’re going to work with custom order statuses programmatically only because I am not a big fan of having an excessive number of WordPress plugins on a single site, especially when it is just enough to deal with a couple of lines of code.
It is also worth noting that my Order Sync and Multisite Order Sync plugins work great with any custom order statuses.
Add a Custom Order Status Programmatically
When registering a status, we are using register_post_status() function, which makes us think that WooCommerce orders are custom post types and WooCommerce order statuses are just post statuses (of course, before HPOS orders appeared). I could prove that with a screenshot of the WordPress database if you wish. Under the post_status column for “orders post type,” you can find statuses like wc-pending, wc-processing, wc-on-hold, wc-completed, wc-cancelled, wc-refunded, wc-failed.
Enough chat; let’s create a custom order status now.
/*
* WooCommerce Custom Order Status
* @author Misha Rudrastyh
* @url https://rudrastyh.com/woocommerce/order-statuses.html#add-custom-order-status
*/
add_action( 'init', 'rudr_register_awaiting_shipping_status' );
function rudr_register_awaiting_shipping_status() {
register_post_status(
'wc-misha-shipping',
array(
'label' => 'Awaiting shipping',
'public' => true,
'show_in_admin_status_list' => true,
'label_count' => _n_noop( 'Awaiting shipping (%s)', 'Awaiting shipping (%s)' )
)
);
}
// Add registered status to list of WC Order statuses
add_filter( 'wc_order_statuses', 'rudr_add_status_to_list' );
function rudr_add_status_to_list( $order_statuses ) {
$order_statuses[ 'wc-misha-shipping' ] = 'Awaiting shipping';
return $order_statuses;
}As a result, you will immediately find the new order status on the edit order page.

Now, let’s take a look at the register_post_status() function parameters.
register_post_status(
'wc-misha-shipping',
array(
'label' => 'Awaiting shipping',
'public' => true,
'show_in_admin_status_list' => true,
'label_count' => _n_noop( 'Awaiting shipping (%s)', 'Awaiting shipping (%s)' )
)
);wc-misha-shippingis just an order status slug, and as you probably noticed, it should begin withwc-(it is a requirement, not a suggestion), because some WooCommerce functions can work only with statuses that begin withwc-. Please also limit it to 20 alphabetical chars and dashes.labelparameter here used without any translation function, but if you create a plugin or a theme, don’t forget to wrap it in__(), like__( 'Awaiting shipping', 'text-domain' ).public– if you set it tofalse, the orders with that status won’t be displayed anywhere, even in the WooCommerce admin.show_in_admin_status_listparameter, when set totrue, allows your custom order status to appear here:

label_countis kind of an interesting parameter in order of what it accepts as its value. It allows you to set text for the admin status list (screenshot above). But you can only pass values via_n_noop()function there, which makes it translation-ready. The first parameter is a singular form, the second one is a plural form.
Everything is clear with the register_post_status() function, right? Let’s jump to the next part of the code.
Filter hook wc_order_statuses allows you to change the array of order statuses before they are displayed anywhere on the website. It also allows you to remove any of the default order statuses, which is not recommended and can be done only with caution. Or you can change their order!
For example, if you would like our custom status to be displayed right before the “Completed” status, you can use this code:
function rudr_add_status_to_list( $order_statuses ) {
$new = array();
foreach( $order_statuses as $id => $label ) {
if( 'wc-completed' === $id ) { // before "Completed" status
$new[ 'wc-misha-shipping' ] = 'Awaiting shipping';
}
$new[ $id ] = $label;
}
return $new;
}You can also use array_splice() and array_merge() functions, if you want.

Add a Custom WooCommerce Order Status to Bulk Actions
It is time to make our custom order status work with the bulk actions dropdown.
To add anything to the bulk actions dropdown, we need to use bulk_action-{screen id}. A little more details about it is in the bulk actions tutorial.
But now, when we are going to use it for the orders page, its screen IDs are going to be:
edit-shop_order– for CPT-based orders (legacy),woocommerce_page_wc-orders– for HPOS orders.
Since the callback function is going to be the same, I recommend to use both hooks: bulk_action-edit-shop_order and bulk_actions-woocommerce_page_wc-orders.
// Add custom order status to the bulk actions dropdown
// CPT-based orders
add_filter( 'bulk_actions-edit-shop_order', 'rudr_register_bulk_action' );
// HPOS orders
add_filter( 'bulk_actions-woocommerce_page_wc-orders', 'rudr_register_bulk_action' );
function rudr_register_bulk_action( $bulk_actions ) {
$bulk_actions[ 'mark_awaiting_shipping' ] = 'Change status to awaiting shipping'; // <option value="mark_awaiting_shipping">Change status to awaiting shipping</option>
return $bulk_actions;
}Once you paste this code to your theme functions.php file (I hope you know when to use or not use child themes), the option will appear in the bulk actions dropdown.

But at this moment, it is useless. I mean, it does nothing when you select it from the dropdown list and push the apply button.
In order to make it work, we have to use another filter hook, which is handle_bulk_actions-{screen id}, so for WooCommerce orders, we are using:
handle_bulk_actions-edit-shop_order– legacy orders,handle_bulk_actions-woocommerce_page_wc-orders– HPOS orders.
Here you go:
add_action( 'handle_bulk_actions-edit-shop_order', 'rudr_bulk_process_custom_status', 20, 3 );
add_action( 'handle_bulk_actions-woocommerce_page_wc-orders', 'rudr_bulk_process_custom_status', 20, 3 );
function rudr_bulk_process_custom_status( $redirect, $doaction, $object_ids ) {
if( 'mark_awaiting_shipping' === $doaction ) {
// change status of every selected order
foreach ( $object_ids as $order_id ) {
$order = wc_get_order( $order_id );
$order->update_status( 'wc-misha-shipping' );
}
// do not forget to add query args to URL because we will show notices later
$redirect = add_query_arg(
array(
'bulk_action' => 'marked_awaiting_shipping',
'changed' => count( $object_ids ),
),
$redirect
);
}
return $redirect;
}On line 10, we are using a slug of a custom order status we created previously. It can be used with or without wc- prefix, by the way, $order->update_status( 'misha-shipping' ) is also ok.
Last but not least, let’s create admin notices.
add_action( 'admin_notices', function() {
if(
isset( $_REQUEST[ 'bulk_action' ] )
&& 'marked_awaiting_shipping' == $_REQUEST[ 'bulk_action' ]
&& isset( $_REQUEST[ 'changed' ] )
&& $_REQUEST[ 'changed' ]
) {
// displaying the message
printf(
'<div id="message" class="updated notice is-dismissible"><p>' . _n( '%d order status changed.', '%d order statuses changed.', $_REQUEST[ 'changed' ] ) . '</p></div>',
$_REQUEST[ 'changed' ]
);
}
} );Works perfectly.

Misha Rudrastyh
Hey guys and welcome to my website. For more than 15 years I've been doing my best to share with you some superb WordPress guides and tips for free.
Need some developer help? Contact me
Very Handy – thanks.
Hi Misha,
This is a great article and really helped me add some order statuses. Is there any way to get the new statuses in the ‘Bulk Action’ menu?
Hi Drew,
it looks like the idea for the new post. Currently I have no ready code. I will reply to your comment when it will be ready (I hope within a week, until Tuesday).
That would be brilliant, thanks.
Hi Drew,
Done! :)
This is awesome! I wish Woocommerce tutorials were even 1/4 as close to your detailed explanation! God bless you, Misha!
Please keep up the good work of helping people!
Thank you very much :)
Hi!
Thanks for the great tutorial. I added the source code in my functions.php of the child theme in WordPress but nothing happens, all options are still available. Is there any change to how this works in the recently released WooCommerce 3? Because I had some renamed before in my functions.php and that has stopped working as well since upgrading to WC3.
Thanks
Alex
Hi Alex,
just made a test — for me it works on WooCommerce 3.0.4
Hi, thank you for this tutorial.
Could you help me, how to I remove an order status when there are only “downloadable products” in the order.
I have a custom order status called “dispatch” which I manually trigger to go to the Logistics department once Accounts can confirm that payment was made successfully. But I do not want this status option in the backend when the order contain only downloadable products.
Thank you
Hey,
I didn’t test this code, but try it out:
Thanks! You are awesome!
hello, thank you very much for this tutorial but I have a question. let’s say I want to show certain statuses for orders that are being shipped by a certain shipping company.
for example if an order is shipped by company ‘x’ then statuses are a, b and c. if an order is shipped by company ‘y’ then statuses are 1,2 and 3. can I do that?
Can this be used to add tracking numbers of multiple orders at once?
This tutorial should help you.
Hello thank you so much for amazing post. can you give me any idea when order status change to custom order status then email to customer.
Hi Misha,
Thanks for the code sample. I have added a custom shipping status and all works. Thank you.
Anyhow here is a question on statuses from a recent-edge case :
Customer had paid and status went from
on holdtoprocessing. However the customer wanted a last minute change of order items ( no cost difference ). We would like to edit those order items but orders are only editable ‘on hold’.I thought to work with the ‘wc_order_is_editable’ filter – and perhaps just always return true. But this is a bit hacky and I would welcome your advice. Simply changing the WC status drop-down has no effect or am I missing something?
Hi Josh,
Hmmm… I think
wc_order_is_editableshould be ok. Just in case someone else need it:Yes I thought that might be the way to go. Works well, thank you.
Hey thanks a lot! This snippet is exactly what i need and it works awesome.
Hi Misha,
I have a custom action on a custom status change, but when I change to this status in a bulk, this action only exec in the first element that I selected, did you know why?
Hi Juan,
Didn’t you forget about foreach?
I, thanks for reply, the foreach is the same that you have in your code (“foreach( $_REQUEST[‘post’] as $order_id )”), and this take every post_id that I selected with checkboxes.
Hmmm… Strange. Please contact me by email and let’s figure it out.
Hi! Great post, thanks! Is it do-able to have custom statuses not automatically be followed up by core statuses? Let’s say a order has status ‘Payment Required’ and customer wants to pay but cancels the payment. Now the order gets status ‘Cancelled’ or ‘Failed’ (core status) automatically but I want it to get a custom status like ‘Payment still Required’ or something.
Thanks for your time!
Hi,
Thank you!
Hmmm… I think you could try this hook
woocommerce_order_status_failedor this onewoocommerce_order_status_cancelled.Thanks for your reply.
I managed to get it to work with following hook:
Hi Misha,
Is it possible to change the order of the items in the bulk actions dropdown? I would like to move the custom status below wc-processing.
Thank you so much!
Hey Eve,
Yes, it is possible with
array_slice()function. Example:Hi Misha,
Thanks for this awesome code! Is there a way to reorder all the statuses in the bulk change list?
Thanks again!
Great work and passion, Misha!
I have successfully integrated your code to add a new order status.
Now, how does one create an email template and trigger the email upon updating an order status to this new order status?
Do you have a tutorial for that or could you recommend a resource?
TIA
Hey Howard,
At this moment I can not recommend you a resource, maybe I will publish a tutorial about it later.
Hello Misha,
First of all, great tutorial up there.
I do have a question though. If I want to have multiple custom order statuses, is there a way I can do that without having to
Hi Misha! Love all your coding help that you write about.
This code used to work for me but it does not work anymore. I’m not sure if this has to do with either the WordPress updates or the plugins that I have installed. I have installed plugins that adds additional bulk actions to the drop down. I also noticed that there is a new one named “Move to Trash” which is at the top of the dropdown.
Are you able to see if this code still works for you using the most up to date WP? Thanks!
Hi and thank you for your comment!
It seems like everything is working on my WordPress 5.3.2 and WooCommerce 3.8.1.
Everything’s working perfect but i have one question … How to change status label of woocommerce orders page [header] too …
Great post as usually!
I had an issue however with setting status from cli (command line).
What I’ve noticed is that you need to “first save” new status in CMS to be able to use it in CLI
wp --user='your-user-admin-here' wc shop_order update 3719 --status='misha-shipping'One more time, thank you a lot for share !
If you ever planning to go to Europe let me know as I own you plenty of coffes ;)
🙏
Code works on Woocommerce 7.8.2 with WordPress 6.2.2.
Hi,
Thanks for this.
I’ve noticed that while the new status shows up in WC, it doesnt show up as a ‘status option’ within AutomateWoo.
If you were to try to add an action to automatically change the order status based on certain triggers the status is not listed amongst the options.
Do you know of a way to get custom status’ showing within AutomateWoo?
Thanks.
Hello,
Can you please update this code to make it also compatible with the new HPOS in woocommerce?
Thanks!
Hello Robert, it is done.
Thanks. The updates for HPOS saved me where other guides were out of date.
But the direct gives me a blank page as it redirects to admin.php without the
pageparam set. I worked around this by adding'page' => 'wc-orders',to my query args, but this seems wrong. Any ideas?Hello,
when i use update_status to update an order’s status to the newly created order status, the order disappears. changing an order to the new order status only works through the ui. do you know why this might be?