Action Hook since 2.3.0
transition_post_status
Fires whenever a post's status changes
Description
transition_post_status is the only hook that reliably tells you a status changed — including the old value — which save_post cannot do.
When it runs
Inside wp_insert_post()/wp_transition_post_status(), before the more specific {$new_status}_{$post_type} hooks.
Signature
do_action( 'transition_post_status', string $new_status, string $old_status, WP_Post $post );Parameters
$new_statusstring — New post status.$old_statusstring — Previous post status.$postWP_Post — Post object.
Examples
Basic
add_action( 'transition_post_status', function( $new_status, $old_status, $post ) {
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
wp_mail( get_option( 'admin_email' ), 'Post published', get_the_title( $post ) );
}
}, 10, 3 );Send a notification only on the actual draft-to-publish transition.
Common Use Cases
- Detect draft-to-publish transitions
- Log status change history
- Trigger workflows on unpublish/trash
- Build scheduled-publish notifications
Common mistakes
- Using save_post to detect 'just published' — it fires on every save with no way to see the previous status
Related hooks
FAQ
How do I detect only the first publish, not every re-save?
Check that $new_status === 'publish' && $old_status !== 'publish' inside transition_post_status.
Source: wp-includes/post.php