Attention Interested in functions, hooks, classes, or methods? Check out the new WordPress Code Reference!

Plugin API/Action Reference/save post


save_post is an action triggered whenever a post or page is created or updated, which could be from an import, post/page edit form, xmlrpc, or post by email. The data for the post is stored in $_POST, $_GET or the global $post_data, depending on how the post was edited. For example, quick edits use $_POST.

Since this action is triggered right after the post has been saved, you can easily access this post object by using get_post($post_id)


Below is a basic example that will send an email every time a post or page is updated on your website.

function my_project_updated_send_email( $post_id ) {

	// If this is just a revision, don't send the email.
	if ( wp_is_post_revision( $post_id ) )

	$post_title = get_the_title( $post_id );
	$post_url = get_permalink( $post_id );
	$subject = 'A post has been updated';

	$message = "A post has been updated on your website:\n\n";
	$message .= $post_title . ": " . $post_url;

	// Send email to admin.
	wp_mail( 'admin@example.com', $subject, $message );
add_action( 'save_post', 'my_project_updated_send_email' );

Custom Post Type: 'book'

Suppose you have a 'book' custom post type and you add the book author, publisher and whether or not the book is in print when editing. Here's how you could save this information as metadata:

 * Save post metadata when a post is saved.
 * @param int $post_id The post ID.
 * @param post $post The post object.
 * @param bool $update Whether this is an existing post being updated or not.
function save_book_meta( $post_id, $post, $update ) {

     * In production code, $slug should be set only once in the plugin,
     * preferably as a class property, rather than in each function that needs it.
    $post_type = get_post_type($post_id);

    // If this isn't a 'book' post, don't update it.
    if ( "book" != $post_type ) return;

    // - Update the post's metadata.

    if ( isset( $_POST['book_author'] ) ) {
        update_post_meta( $post_id, 'book_author', sanitize_text_field( $_POST['book_author'] ) );

    if ( isset( $_POST['publisher'] ) ) {
        update_post_meta( $post_id, 'publisher', sanitize_text_field( $_POST['publisher'] ) );

    // Checkboxes are present if checked, absent if not.
    if ( isset( $_POST['inprint'] ) ) {
        update_post_meta( $post_id, 'inprint', TRUE );
    } else {
        update_post_meta( $post_id, 'inprint', FALSE );
add_action( 'save_post', 'save_book_meta', 10, 3 );

See also quick_edit_custom_box: Creating Inputs.

NOTE: As of WP 3.7, an alternative action has been introduced, which is called for specific post types: save_post_{post_type}. Hooking to this action you wouldn't have to check on the post type (ie: if ( $slug != $_POST['post_type'] ) in the sample above).

Avoiding infinite loops

If you are calling a function such as wp_update_post that includes the save_post hook, your hooked function will create an infinite loop. To avoid this, unhook your function before calling the function you need, then re-hook it afterward.

// this function makes all posts in the default category private

function set_private_categories($post_id) {
	// If this is a revision, get real post ID
	if ( $parent_id = wp_is_post_revision( $post_id ) ) 
		$post_id = $parent_id;

	// Get default category ID from options
	$defaultcat = get_option( 'default_category' );

	// Check if this post is in default category
	if ( in_category( $defaultcat, $post_id ) ) {
		// unhook this function so it doesn't loop infinitely
		remove_action( 'save_post', 'set_private_categories' );

		// update the post, which calls save_post again
		wp_update_post( array( 'ID' => $post_id, 'post_status' => 'private' ) );

		// re-hook this function
		add_action( 'save_post', 'set_private_categories' );
add_action( 'save_post', 'set_private_categories' );

NOTE: It is very important to use the same parameters in remove_action than in add_action. Example with priority below (extract)... if the parameters are not the same, the infinite loop occurs... :-(

// unhook this function so it doesn't loop infinitely
		remove_action( 'save_post', 'set_private_categories', 13, 2 );

		// update the post, which calls save_post again
		wp_update_post( array( 'ID' => $post_id, 'post_status' => 'private' ) );

		// re-hook this function
		add_action( 'save_post', 'set_private_categories', 13, 2 );
add_action( 'save_post', 'set_private_categories', 13, 2 );

Change Log

Source File

Triggered by wp_insert_post and wp_publish_post in wp-includes/post.php