publish_post Runs when a post is published, or if it is edited and its status is "published". Action function arguments: post ID.

-Plugin API Documentation

I've added the publish_post hook to a WordPress plugin that I'm writing. The function called by the hook itself, is meant to change the categories of several posts using the wp_update_post function.

This hook does not work however as the result returned from running wp_update_post is always 0. My best guess is that running wp_update_post causes another instance of my hook to run because it re-publishes the post...which I believe brings about the "...or if it is edited and its status is "published"" of the statement above.

Is there any other action-hook that I can use that will only be called when the post added is completely new and not edited?

 Plugin Name: Category Switcher Plugin
 Plugin URI: http://www.example.com
 Description: When a new post is created this plugin will cause the 
 Version: 0.1
 Author: Me
 License: GPL2 
class categoryShifter {
  function shiftCategories($post_ID) {

    $maxNumPostsFirstTeir = 4;

    $first_teir_cat = "Fresh News Stories 1";
    $second_teir_cat = "Slightly Dated Stories 2";  

    $firephp = FirePHP::getInstance(true);

    $firephp->info('BEGIN: categoryShifter.shiftCategories()');

    $firephp->log($post_ID, 'post_ID: ');
    $firephp->trace('trace to here');    

    $first_teir_id = categoryShifter::getIDForCategory($first_teir_cat, $firephp); 
    $second_teir_id = categoryShifter::getIDForCategory($second_teir_cat, $firephp);

    $firephp->log($first_teir_id, '$first_teir_id');
    $firephp->log($second_teir_id, '$second_teir_id');   

    $qPostArgs = array(
      'numberposts' => 100,
      'order' => 'DESC', 
      'orderby' => 'post_date',
      'post_type' => 'post',
      'post_status' => 'published', 
      'category_name' => $first_teir_cat

    $firstTeirPosts = get_posts($qPostArgs);   
    $firephp->log($firstTeirPosts, 'got posts:');

    $firephp->log(sizeof($firstTeirPosts), 'sizeof');

    // NOTE: This appears to work.
    for($i = sizeof($firstTeirPosts)-1; $i > $maxNumPostsFirstTeir-4; $i--) 
      $newCats = array($second_teir_id);
      $editingId = $firstTeirPosts->ID;
      $result = wp_set_post_categories($editingId, $newCats); /* NOTE: Doesn't work presently... returns an array with the $second_teir_id in it. */
      $firephp->log($result, 'Result'); 

    $my_post = array();
    $my_post['ID'] = 132;
    $my_post['post_category'] = array($second_teir_id);

    $firephp->log('Before', 'Before'); 
    if(wp_update_post( $my_post ) == 0) {
        $firephp->Error('Fatal Error, Post not updated', 'error');
    $firephp->log('After', 'After');
    return $post_ID;

  function getIDForCategory($cat_name, $logger) {
    $logger->Info("Begin: getIDForCategory()");

    $cats = get_categories();      

    $whichCatId = "";

    foreach($cats as $single_cat) {
      if($single_cat->name == $cat_name) {
       $whichCatId = $single_cat->term_id;
    $logger->Info("End: getIDForCategory()");
    return (int)$whichCatId;

/* Hook Post Creation */
/* add_action('publish_post', array('categoryShifter','shiftCategories')); */
add_action('wp_insert_post', array('categoryShifter', 'shiftCategories'));

I've switched to using the wp_insert_post hook for the time being...but I still can't get the wp_set_post_categories function to change the categories of the posts.

I understand that I will probably need to update this code so that it takes into account the existing categories of the post and only modifies the ones specified by the plugin, but for now it's really just an alpha.

  • 1
    @leeand00 - Can you post your code as an update to your answer so we can be better able to see what needs correcting? Commented Feb 1, 2011 at 5:49
  • @Mike Here's the update!
    – leeand00
    Commented Feb 1, 2011 at 6:12
  • @MikeSchinkel Sometimes that just because I haven't gotten the chance to try the answers out yet, and I didn't want to mark one that's not a good answer....but I'll go back and see what I can do!
    – leeand00
    Commented Feb 1, 2011 at 16:28
  • @MikeSchinkel Okay I cleaned them up and selected the best answers, sorry about that, I didn't realise how I had neglected this particular StackExchange.
    – leeand00
    Commented Feb 1, 2011 at 16:34

6 Answers 6

add_action('new_to_publish', 'your_function');
add_action('draft_to_publish', 'your_function');
add_action('pending_to_publish', 'your_function');
  • 3
    How about the wp_insert_post action? core.trac.wordpress.org/browser/tags/3.3.1/wp-includes/…
    – soulseekah
    Commented Mar 23, 2012 at 9:56
  • 1
    But will these run again if, for example, status is changed from published back to draft then to published again?
    – brandonjp
    Commented Jul 27, 2019 at 16:07
  • @soulseekah wp_insert_post hook will be called again when edit existing post and save. Tested on WordPress 5.9. I know your comment is 2012 but in case some one curious about this.
    – vee
    Commented Feb 1, 2022 at 6:32
  • @vee, probably drafts being created on every save.
    – soulseekah
    Commented Feb 2, 2022 at 8:43

I have extensively read the WordPresss core and I have tried it all. wp_transition_post_status(), new_to_publish(), new_{post_type}(), wp_insert_post().

All of these are, after all, unreliable.

wp_transition_post_status() isn't reliable because the new status "publish" is the default status both for creating new posts and updating existing ones. Old status isn't reliable to define what's a new post and what's not because it can be draft, auto-draft, publish, etc.

new_to_publish() doesn't work for custom post types.

new_{post_type} only passes $post as parameter, and you can't know if it's new or updating existing one

wp_insert_post() have a $update parameter that should be TRUE if updating existing posts and FALSE if creating new posts, but it's unreliable because it returns TRUE for new posts because of auto-draft.

Solution: Using a post meta

I ended up using a custom post meta that I called check_if_run_once that will execute some logic only one time:

*   Do something when a new book is created
function new_book($post_id, $post, $update) {
    if ( $post->post_type == 'book' && $post->post_status == 'publish' && empty(get_post_meta($post_id, 'check_if_run_once')) ) {
        # New Post

        # Do something here...

        # And update the meta so it won't run again
        update_post_meta( $post_id, 'check_if_run_once', true );
add_action( 'wp_insert_post', 'new_book', 10, 3 );

Optionally, if you need to update your existing posts with the "check_if_run_once" meta, so it don't run the code above for existing posts created before you added that function, you could do:

*   This is a temporary function to update existing posts with the "check_if_run_once" post meta
*   Access your website logged in as admin and add ?debug to the URL.
function temporary_function() {
    if (current_user_can('manage_options')) {
        $posts = get_posts(array(
            'post_type' => 'book',
            'posts_per_page' => -1, // You might need to paginate this depending on how many posts you have
            'fields' => 'ids'
        foreach($posts as $post_id) {
            update_post_meta($post_id, 'check_if_run_once', true);
if (isset($_GET['debug'])) {
    add_action('init', 'temporary_function');
  • 2
    Why isn't this upvoted more and the accepted answer. Accepted answer isn't explaining stuff and doesn't keep in mind that people tend to use this for CPT etc.. Commented Jul 8, 2021 at 18:00
  • Thanks man, really. What an answer! Commented Feb 27 at 14:16

Precisely target creation of new post is actually more tricky than it seems. Technically there are multiple ways post can get created or updated and there are plenty not so obvious things that are technically posts as well (revisions for example).

WordPress provides dynamic hooks that track not only post creation, but what it was and what it became. See Post Status Transitions in Codex.


More using experimenting than following the docs, this works for me (WP 3.3). I get a transition_post_status hook call with $new_status set to "auto-draft" when you create a new post.

function my_post_new($new_status, $old_status=null, $post=null){
    if ($new_status == "auto-draft"){
        // do stuff here
add_action('transition_post_status', 'my_post_new');
  • 1
    This will fire off every time an auto-draft is saved, i.e. every minute or so when editing.
    – soulseekah
    Commented Mar 23, 2012 at 9:54
  • Good point. In my case this is not an issue but it could be for others, depending on what you were doing.
    – PapaFreud
    Commented Mar 23, 2012 at 10:50
  • i wanted to upvote, but i can't yet. so props go here, @PapaFreud . btw, this is now documented at codex.wordpress.org/Post_Status_Transitions . again, thanks man
    – Ana Ban
    Commented May 22, 2012 at 12:30
  • 1
    @soulseekah Interesting point you raised, I tested this, and auto-draft is assigned as the $new_status of a post upon its creation (i.e. Add New), once only. As soon as the first AUTOSAVE procedure fires after 1 minute, the $new_status value is updated to draft. The $old_status is still NULL until the post is manually saved or published. So technically this would work and not fire each time an the editor auto-saves your work. As an additional measure you could check to ensure that the $old_status is NULL to verify -> if ($new_status == "auto-draft" && $old_status === NULL)
    – Adam
    Commented May 22, 2013 at 13:57

I found the best option was to check the post status when wp_insert_post is called.

When a new post is created, the initial status is auto-draft.

if (!function_exists('benyonsFunction')) {
    function benyonsFunction($postId, $post) {
        if ($post->post_status == "auto-draft") {
            error_log('NEW POST!!!');

add_action('wp_insert_post', 'benyonsFunction', 10, 2);

This error_log will only ever fire once.


The most clean and reliable way to handle this for a known post type is to use the {$new_status}_{$post->post_type} hook which provides the old status and compare it to "publish".

For example, if you wanted to fire when a new "page" is created you would use:

add_action( 'publish_page', function( int $post_id, \WP_Post $post, string $old_status ) {
    if ( 'publish' === $old_status ) {
        // Nothing to see here.
    // Business logic goes here.
}, 10, 3 );
  • No this will fire every time a page goes from any status to 'publish' so this will fire way more times than only on the first creation. Commented Mar 31, 2023 at 13:42
  • Why is your page going to "publish" more than once? Once a page is published is it live and the status typically does not change unless you are moving it "trash" to remove it from the site.
    – Mat Lipe
    Commented Apr 1, 2023 at 14:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.