save_postaction-hookWP 1.5.0

Fires whenever a post or page is created or updated, including publication through import, XML-RPC, or email.

Post data is passed in the second $post parameter, but it can also usually be obtained through $_POST, $_GET, or the global global $post_data variable. Which source is available depends on how the post is edited. Quick Edit, for example, uses $_POST.

The hook fires at the very end, after the post data has been updated, the cache has been cleared, and other actions have completed.

The hook fires after the post object cache has been cleared. Therefore, if post data is changed on this hook, clear the post cache again:

// Clear the post cache
clean_post_cache( $post_ID );

Alternatively, use wp_insert_post_data or wp_insert_attachment_data. Only these hooks fire before the cache is cleared, but they do not run when wp_publish_post() is called.

Since version 3.7, the identical save_post_(post_type) hook is available. Replace post_type with a post type name, and the event fires only when a post of that type is saved or added. The hook looks like this:

do_action( "save_post_{$post->post_type}", $post_ID, $post, $update );

save_post is a copy of the wp_insert_post hook.

The edit_post hook is similar to save_post, but fires only when a post is updated.

Warning: infinite loops

Use wp_update_post() carefully inside the save_post hook. It invokes this hook again, causing an infinite loop. Example #3 below shows how to avoid this. The issue is also described in the wp_update_post() documentation.

Usage

add_action( 'save_post', 'wp_kama_save_post_action', 10, 3 );

/**
 * Function for `save_post` action-hook.
 * 
 * @param int     $post_id Post ID.
 * @param WP_Post $post    Post object.
 * @param bool    $update  Whether this is an existing post being updated.
 *
 * @return void
 */
function wp_kama_save_post_action( $post_id, $post, $update ){

	// action...
}
$post_id(int)
ID of the post being updated.
$post(object)
The post object being updated. It is the same type of object normally stored in the global $post variable.
$update(bool)
true when an existing post is updated.
false when a new post is added.

Examples

#1 Send an email when a post is updated

This demonstration sends the site administrator an email notification whenever a post is updated:

add_action( 'save_post', 'my_project_updated_send_email' );
function my_project_updated_send_email( $post_id ) {

	// Do not send an email for revisions
	if ( wp_is_post_revision( $post_id ) || get_post($post_id)->post_status != 'publish' )
		return;

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

	$message = "The following post was updated on your site:\n\n";
	$message .= $post_title . ": " . $post_url;

	// Send the email.
	wp_mail( get_option('admin_email'), $subject, $message );
}

#2 The custom book post type

Suppose you have a book post type and need to add information about the author, publisher, and print availability. This code demonstrates how to store those values in post metadata:

/**
 * Saves post metadata when the post is saved.
 */
add_action( 'save_post', 'save_book_meta' );
function save_book_meta( $post_id ) {
	// Define the slug once and reuse it throughout code related
	// to the post type, as is customary in classes
	$slug = 'book';

	// Check the post type and return if it is not book.
	if ( $slug != $_POST['post_type'] )
		return;

	// Update post metadata.

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

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

	// inprint is a checkbox.
	if ( isset( $_REQUEST['inprint'] ) ) {
		update_post_meta( $post_id, 'inprint', TRUE );
	} else {
		update_post_meta( $post_id, 'inprint', FALSE );
	}
}

In this example, the if ( $slug != $_POST['post_type'] ) check can be replaced by using the save_post_book event instead of save_post.

#3 Avoid an infinite loop

Calling wp_update_post() inside the save_post event causes an infinite loop because wp_update_post() invokes save_post. To prevent this, remove the attached hook before calling wp_update_post(), then add it again afterward:

// This function makes all posts in the default category private
add_action( 'save_post', 'set_private_categories' );
function set_private_categories( $post_id ){
	// Obtain the real post ID if this is a revision
	if ( $parent_id = wp_is_post_revision( $post_id ) )
		$post_id = $parent_id;

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

	// Check whether the post is in the default category
	if ( in_category( $defaultcat, $post_id ) ) {
		// Remove the hook to prevent an infinite loop
		remove_action( 'save_post', 'set_private_categories' );

		// Update the post. The save_post event fires at this point
		wp_update_post( array( 'ID' => $post_id, 'post_status' => 'private' ) );

		// Add the hook back
		add_action( 'save_post', 'set_private_categories' );
	}
}

This part of the code is required:

// Remove the hook to prevent an infinite loop
remove_action( 'save_post', 'set_private_categories' );

// Update the post. The save_post event fires at this point
wp_update_post( array( 'ID' => $post_id, 'post_status' => 'private' ) );

// Add the hook back
add_action( 'save_post', 'set_private_categories' );

Changelog

Since 1.5.0 Introduced.

Where the hook is called

wp_insert_post()
save_post
WP_Customize_Manager::trash_changeset_post()
save_post
wp_publish_post()
save_post
wp-includes/post.php 5286
do_action( 'save_post', $post_id, $post, $update );
wp-includes/class-wp-customize-manager.php 3121
do_action( 'save_post', $post->ID, $post, true );
wp-includes/post.php 5464
do_action( 'save_post', $post->ID, $post, true );

Where the hook is used in WordPress

wp-includes/default-filters.php 582
add_action( 'save_post', 'delete_get_calendar_cache' );