Blog

Add a WordPress Custom Bulk Action

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 FoxRunSoftware Custom Bulk Action Demo Plugin

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

50 Comments

  • Matth Jenks 4 months ago Reply

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

    • justin 4 months ago Reply

      You’re welcome, it was a big pain for me to figure out the first time, so my hope is that the next person doesn’t have to waste half a day on it.

      • ravin 1 month ago Reply

        Bless you.

  • nate 3 months ago Reply

    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… : )

    • justin 3 months ago Reply

      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!

      • nate 3 months ago Reply

        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!

        • justin 3 months ago Reply

          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!

  • Matth Jenks 3 months ago Reply

    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.

  • Matth Jenks 3 months ago Reply

    There’s also this plugin which looks promising:

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

  • Todd Crowe 3 months ago Reply

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

    • justin 3 months ago Reply

      You are very welcome Todd, glad it helped!

  • Matteo 3 months ago Reply

    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 :)

    • justin 3 months ago Reply

      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'

      • Jay 3 months ago Reply

        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?

        • Max G J Panas 2 months ago Reply

          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’ );

  • Andrew 3 months ago Reply

    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’))

    );

    • Jay 3 months ago Reply

      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

      • Max G J Panas 2 months ago Reply

        Hey Jay,

        you are going to need to add the custom bulk upload before the table gets built, so putting it in admin footer like you tried is a no go.

        Instead, you can hijack the ‘admin_head’ action and put the code in there. $wp_list_table is defined by this point so adding to it like so is cake.

        • Jepser Bernardino 25 weeks ago Reply

          Sir, so much thanks, it was thinking that the Javascript option wan’t the best way to do it.

          • Jepser Bernardino 25 weeks ago

            Just a problem, it converts the action and action2 in action_2, so bulk actions don’t work.

  • Ayoub 3 months ago Reply

    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'];

  • obmerk99 3 months ago Reply

    Your Zip file for the plugin returns 404

    • Max Rice 3 months ago Reply

      Fixed now, thanks :)

  • Ross McKay 3 months ago Reply

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

  • Leo 3 months ago Reply

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

  • vik 2 months ago Reply

    Thanks, great work!

  • Hugh Lashbrooke 2 months ago Reply

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

    • Justin Stern 2 months ago Reply

      Glad to hear it!

  • Miguel 2 months ago Reply

    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.

    • Max Rice 2 months ago Reply

      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!

  • mk 2 months ago Reply

    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”
    }
    }

  • Toptan Karanfil 1 month ago Reply

    Thank you very useful information

  • Tara 1 month ago Reply

    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!

  • Marcel de Hoog 1 month ago Reply

    Seems no longer working in 3.7.1 :(

  • Robert Ruby II 1 month ago Reply

    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)

    • Beka Rice 1 month ago Reply

      happy to help!

    • Marcel de Hoog 1 month ago Reply

      Robert did you get this to work? If so which version are you using, as in 3.7.1 I cannot get it to work at all. $wp_list_table->current_action() does return empty.

  • Manish 20 weeks ago Reply

    $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…

    • Max Rice 18 weeks ago Reply

      Where are you hooking in that $wp_list_table->current_action() doesn’t return anything?

  • Matthew 14 weeks ago Reply

    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?

  • Norman 14 weeks ago Reply

    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

    • Beka Rice 14 weeks ago Reply

      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!

  • Antonie 10 weeks ago Reply

    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!

  • Mario 5 days ago Reply

    Hi

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

  • sakib 3 days ago Reply

    $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.

    • sakib 3 days ago Reply

      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.

  • sakib 3 days ago Reply

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

Submit a Comment