WP Development
Good news! There are now core APIs to do this as of WordPress 4.7. You can read more here.

Like me you probably woke up this morning assuming that adding a new bulk action to WordPress would be as easy tracking down the correct filter or implementing the right action hook. If so, then you might have had a rude awakening when you attempted to make use of the promising-looking filter named bulk-actions-screenid and failed. Thanks to a couple of posts on the subject I was able to create my own custom bulk actions, however it took enough of my time that I decided to put together a little article on the subject, and even include a full-fledged working demo plugin to hopefully make your own bulk actions that much easier to get going.

The Goal

The goal here is to describe and provide code that allows you to easily add your own custom bulk actions to any part of the WordPress admin: posts, media, users, whatever. The approach presented is based on this wordpress answers page. For this example, we’ll pretend we’re adding a custom action named “Export” to the posts page, to allow the bulk exporting of selected posts. Note that if you want to add custom bulk actions to one of the other sections, say the Users page, you’ll have to modify my code in a couple of places which I identify in the demo plugin comments. Our final result should look like this:

The Issue

That promising-looking filter bulk-actions-screenid currently only has the ability to remove bulk actions, not add them. This is clearly noted on the filters codex page, and in a code comment in the file wp-admin/includes/class-wp-list-table.php. As described in this trac ticket the issue isn’t so much how to add a new bulk action to the dropdown menu, as it is how to generically handle custom bulk actions. Since this is yet to be solved in the core, it requires a bit of hacking; however don’t worry no core files were harmed in the making of this plugin.

The Solution

Enough of the problems, on to the solution. Creating a custom bulk action breaks down to three components. First, adding an item to the bulk action select box:

add_action('admin_footer-edit.php', 'custom_bulk_admin_footer');

function custom_bulk_admin_footer() {

  global $post_type;

  if($post_type == 'post') {
    ?>
    <script type="text/javascript">
      jQuery(document).ready(function() {
        jQuery('<option>').val('export').text('<?php _e('Export')?>').appendTo("select[name='action']");
        jQuery('<option>').val('export').text('<?php _e('Export')?>').appendTo("select[name='action2']");
      });
    </script>
    <?php
  }
}

As you can see, here we use javascript to inject our new bulk action item into the two bulk action select dropdowns on the Posts edit page. This piece of code certainly isn’t poetry, but until a filter capable of adding items is added to the core, it will have to do.

The next step is the code that performs the bulk action. This is done with a hook on the load-* actions, as recommended in the Q&A page referenced above. Even with demo code, this function is fairly lengthy so I’ll leave most of it out, please reference my demo plugin for the full working implementation. Its main tasks are to:

  1. determine the action
  2. perform security checks
  3. perform the action
  4. redirect the client to a results page
add_action('load-edit.php', 'custom_bulk_action');

function custom_bulk_action() {

  // ...

  // 1. get the action
  $wp_list_table = _get_list_table('WP_Posts_List_Table');
  $action = $wp_list_table->current_action();

  // ...

  // 2. security check
  check_admin_referer('bulk-posts');

  // ...

  switch($action) {
    // 3. Perform the action
    case 'export':
      // if we set up user permissions/capabilities, the code might look like:
      //if ( !current_user_can($post_type_object->cap->export_post, $post_id) )
      //  pp_die( __('You are not allowed to export this post.') );

      $exported = 0;

      foreach( $post_ids as $post_id ) {
        if ( !$this->perform_export($post_id) )
          wp_die( __('Error exporting post.') );
        $exported++;
      }

      // build the redirect url
      $sendback = add_query_arg( array('exported' => $exported, 'ids' => join(',', $post_ids) ), $sendback );

    break;
    default: return;
  }

  // ...

  // 4. Redirect client
  wp_redirect($sendback);

  exit();
}

The third step is to display a notice describing the result of the action by hooking into admin_notices and rendering a message if we have the ‘exported’ request parameter that was set in the previous block of code:

add_action('admin_notices', 'custom_bulk_admin_notices');

function custom_bulk_admin_notices() {

  global $post_type, $pagenow;

  if($pagenow == 'edit.php' && $post_type == 'post' &&
     isset($_REQUEST['exported']) && (int) $_REQUEST['exported']) {
    $message = sprintf( _n( 'Post exported.', '%s posts exported.', $_REQUEST['exported'] ), number_format_i18n( $_REQUEST['exported'] ) );
    echo "<div class="updated"><p>{$message}</p></div>";
  }
}

Which in action looks like:

Demo Plugin

And there you have it. Feel free to download and use as a starting point my demo plugin which has code similar to the above, but fully implemented and commented. Install like any other plugin, by uploading through the Admin, or by unzipping the plugin folder and placing in wp-content/plugins/. Activate the plugin, then go to Posts > All Posts and try out the ‘Export’ bulk action.

Download Custom Bulk Action Demo Plugin

If you have any issues, or suggestions to improve the code, please leave a comment below.

Published by Justin Stern

Justin is one of our co-founders, and is our resident overengineer. He likes to write developer tutorials and make black magic happen in our plugins. He thinks that writing code is a lot easier than writing words.

61 Comments

  1. Wow, thanks for sharing this. I really didn’t expect a google search to turn up anything at all.

  2. My goal is to find code that allows me to bulk email data – Say I have columns (project) (link to project page )(contact_name) (contact_email) (artist_assigned) (art_due_date) (in-hands_date)(project_client_status)(in_house_status)… I would like to bulk email data from table to both client and artist – the client would receive a pre formatted email that sends automatically with status changes and also a bulk email option that simply can be sent out manually by selecting projects and applying bulk action… The other email would be to artists who are say invited to take on a project and then they can actually select their name from drop-down inside the table (artist_assigned) and assign themselves… I know this might be a huge task for me – a beginner – but i have most everything but the email action figured out ( any clues or posts you can point me to ? ) – sorry so long! Perhaps I just need to get to reading and looking into more PHP training… : )

    • Hey Nate, that is a pretty substantial project you’re describing, and you may well find that you need to learn some more PHP. Hopefully you should be able to follow and replicate the steps I describe above, and then in your action function you can send emails using the wp_mail() function. Best of luck with your project!

      • great – So I’m still researching and my answer so far is to try using Piklist when ready – Have you played with Piklist yet? Looks promising! Of course some php knowledge will go a long way when this application builder framework is stable!

        • Nope, never heard of Piklist, hopefully it works out for you! I’d be curious to hear back from you here if it does, it would be good to know it’s worth recommending to others. Good luck!

  3. Nate, you might want to look into Project Management plug-ins for wordpress. Some of them, like CollabPress, have the ability to e-mail certain collaborators on a project at each milestone.

    Another option is to set up a Basecamp account. It’s not free but it might be easier and cheaper than developing your own custom app.

  4. There’s also this plugin which looks promising:

    http://smartypantsplugins.com/sp-client-document-manager/

  5. This is a great article. It was exactly what I was looking for and saved me a lot of time. Thank you!

  6. Hello there, thanks for your precious tutorial!

    Anyway I have a question for you… I see that “admin_footer-edit” hookworks well for posts, but what if I need to add my custom bulk action to the media gallery list (“upload.php”)? Which hook should I use? Tried a million google searches with no success…

    Thanks in advance 🙂

    • Hey Matteo you would hook into the 'admin_footer-upload.php' action, and if you’re checking the $post_type as I do, it would be 'attachment'

      • I really appreciate this tutorial. It seems to be about the only thing on the web for doing this–all the other places online link back here.

        Like Matteo, I also am trying to adapt your excellent code for the media page. I got it to work there using the adaptations you suggest here and in the file itself (v0.1, 4/4/2012), but only if I comment out a conditional (L58) and the security check (L68) in `custom_bulk_action()`, viz.

        `if($post_type == ‘post’) {`

        `check_admin_referer(‘bulk-posts’);`

        I tried replacing `’post’` with `’attachment’`, but it turns out that the problem is that `$post_type = $typenow` is always false, even if the operation (without the conditional) runs successfully.

        Any ideas on how I can get these lines to work?

        • Hey, the security checks are Always important if you want to minimize misuse of the bulk action. So you’re going to want to keep them around. For the media page, here’s how:

          change the conditional to:
          if ( ! isset( $_REQUEST[‘detached’] ) ) {

          and the sec check to:
          check_admin_referer( ‘bulk-media’ );

        • Hai,
          Just found out that the test on post_type might fail because sometimes post_type is an array of values.
          You should make an array of post_type if it is not and then use in_array() to test for the correct post_type.

  7. Add a bulk action without JavaScript:

    global $wp_list_table;

    $wp_list_table->_actions = array_merge(

    $wp_list_table->get_bulk_actions(),

    array(‘new_action’ => __(‘My new action’))

    );

    • Hi Andrew,

      Interesting comment–could be very helpful. Could you supply a little context? I’ve naively just replaced the contents of custom_bulk_admin_footer() with what you’ve written and it doesn’t work.

      It would be appreciated.

      Jay

    • whats the changes we have to make to implement custom-bulk-action on the users page

      Raj

  8. Since post_ids are in the URL …&post[]=13660&post[]=13613&post[]=13608&post[]=13564…

    This is how you get it:

    $post_ids = $_GET[‘post’];

  9. Your Zip file for the plugin returns 404

  10. Thanks, mate! A big help on a project I just completed. cheers!

  11. This was a life-saver. I used it on my theme, Symbiostock.

  12. Thanks, great work!

  13. Thanks for this guide – your demo plugin got me sorted out in no time.

  14. Hi, Justin. Thanks for the article. I am really really new to this, and I am trying to learn as much as I can before venturing into asking dumb questions. But, here it goes: in your example, you create a bulk action item to be displayed in the dropdown menu. Now, after you select several posts and click on Export, what happends exactly to those posts. If this question is too basic, just direct me to another resource on the web, please. Thanks for your help.

    • The post IDs of the posts you selected are included in the $_POST variable so that your code can perform some action with each of them. Hope that helps!

  15. Hi,

    I am getting empty in this $action = $wp_list_table->current_action(); with wp 3.7.1
    Could you tell me what I should check?
    This is the $_POST I get after I click the apply button.
    array(8) {
    [“s”]=>
    string(6) “test”
    [“post_status”]=>
    string(3) “all”
    [“post_type”]=>
    string(4) “post”
    [“m”]=>
    string(6) “201312”
    [“cat”]=>
    string(1) “0”
    [“paged”]=>
    string(1) “1”
    [“mode”]=>
    string(4) “list”
    [“post”]=>
    array(2) {
    [0]=>
    string(6) “440089”
    [1]=>
    string(6) “440090”
    }
    }

  16. Thank you very useful information

  17. Hello! Thank you so much for this code.

    I do have a question though. How do I change this so that it works on the users page? I tried very hard but I couldn’t figure out what to change the commented sections to. Thank you!

  18. Seems no longer working in 3.7.1 🙁

  19. Thanks a bunch. Developing a management system for my engineering and construction company using wp as an SDK.. Need to pull reports for clients of db and this little nugget right here made my life easier… was about to say the hell with it and create a new list page just for jobs… (main custom post type in program)

  20. $wp_list_table->current_action() does not return anything and when I call $wp_list_table->get_bulk_actions() then return only the previous options value..
    Please help me…

  21. I am trying to do this but I would like to make it disable the plugin updates and than have another to enable them again. Is this possible? If so, how do I do it?

  22. Hi, Seems to be a great plugin. I would like to add “EMAIL” instead of “Export”. Can you tell me what t change in the plugin for it to work.
    Thanks for your hard work

    • Hey Norman! Doing this for emails would be a different implementation that would take far more to explain than we could in a comment, but I’ve added this to our post idea board for a future tutorial!

  23. Very good article, excellent.

    I used this to build several bulk actions under the Users section of WordPress such as exporting, bulk updating user meta with a value, etc.

    Thanks again!

    • I am trying to adapt this to work with the Users page as well.
      Can you give me any pointers on what items need to be changed?

  24. Hi

    How do I actually EXPORT the data? I can’t seem to get any file to download…

  25. $wp_list_table->current_action() returns empty, so this solution won’t work for current version of wordpress(3.9). Maybe adding query variable will work instead.

    I will let you know if I can get it to work.

    • After further investigation, I came to the conclusion that your code works alright.

      The problem that many people(including myself) face is that, nothing is printed if they try to print $action or any other text. The reason for that is there is another redirect there.

      If you look at wp-includes/edit.php, you will see that there is something like this
      wp_redirect($sendback);

      Hope this will help others.

  26. There is a wp_redirect($sendack) at the end of this script. How did I miss that!

  27. Thanks Justin, this was very helpful.

  28. Hi,

    How can I use this bulk action plugin in users page where these is no post type kind of thing?

    Thanks

  29. I can’t seem to grasp how one can use wc_add_notice() before the redirect in the bottom of the function custom_bulk_action(). I keep getting an error: Fatal error: Call to undefined function wc_add_notice()

    I won’t to show messages and then redirect. Can this be done?

    Thank you so much for a nice tutorial!

  30. Can this code be updated to apply the bulk action to all items?

    I know you can change the Number of Items Per Page setting in Screen Options, but I then run into “URL too long” errors when running the bulk action.

    • I think if you wanted a bulk action to apply to all items of a specific post type, then you’d have to create a custom action handler to first find all those items, then apply the bulk action.

  31. How to verify nonce in users.php bulk actions?

Hmm, looks like this article is quite old! Its content may be outdated, so comments are now closed.