wp_update_post( array|object $postarr = array(), bool $wp_error = false, bool $fire_after_hooks = true ): int|WP_Error

Updates a post with new post data.


The date does not have to be set for drafts. You can set the date and it will not be overridden.


Post data. Arrays are expected to be escaped, objects are not. See wp_insert_post() for accepted arguments.
Default array.
More Arguments from wp_insert_post( … $postarr )An array of elements that make up a post to update or insert.
  • ID int
    The post ID. If equal to something other than 0, the post with that ID will be updated. Default 0.
  • post_author int
    The ID of the user who added the post. Default is the current user ID.
  • post_date string
    The date of the post. Default is the current time.
  • post_date_gmt string
    The date of the post in the GMT timezone. Default is the value of $post_date.
  • post_content string
    The post content. Default empty.
  • post_content_filtered string
    The filtered post content. Default empty.
  • post_title string
    The post title. Default empty.
  • post_excerpt string
    The post excerpt. Default empty.
  • post_status string
    The post status. Default 'draft'.
  • post_type string
    The post type. Default 'post'.
  • comment_status string
    Whether the post can accept comments. Accepts 'open' or 'closed'.
    Default is the value of 'default_comment_status' option.
  • ping_status string
    Whether the post can accept pings. Accepts 'open' or 'closed'.
    Default is the value of 'default_ping_status' option.
  • post_password string
    The password to access the post. Default empty.
  • post_name string
    The post name. Default is the sanitized post title when creating a new post.
  • to_ping string
    Space or carriage return-separated list of URLs to ping.
    Default empty.
  • pinged string
    Space or carriage return-separated list of URLs that have been pinged. Default empty.
  • post_parent int
    Set this for the post it belongs to, if any. Default 0.
  • menu_order int
    The order the post should be displayed in. Default 0.
  • post_mime_type string
    The mime type of the post. Default empty.
  • guid string
    Global Unique ID for referencing the post. Default empty.
  • import_id int
    The post ID to be used when inserting a new post.
    If specified, must not match any existing post ID. Default 0.
  • post_category int[]
    Array of category IDs.
    Defaults to value of the 'default_category' option.
  • tags_input array
    Array of tag names, slugs, or IDs. Default empty.
  • tax_input array
    An array of taxonomy terms keyed by their taxonomy name.
    If the taxonomy is hierarchical, the term list needs to be either an array of term IDs or a comma-separated string of IDs.
    If the taxonomy is non-hierarchical, the term list can be an array that contains term names or slugs, or a comma-separated string of names or slugs. This is because, in hierarchical taxonomy, child terms can have the same names with different parent terms, so the only way to connect them is using ID. Default empty.
  • meta_input array
    Array of post meta values keyed by their post meta key. Default empty.
  • page_template string
    Page template to use.


Whether to return a WP_Error on failure.


Whether to fire the after insert hooks.



int|WP_Error The post ID on success. The value 0 or WP_Error on failure.


function wp_update_post( $postarr = array(), $wp_error = false, $fire_after_hooks = true ) {
	if ( is_object( $postarr ) ) {
		// Non-escaped post was passed.
		$postarr = get_object_vars( $postarr );
		$postarr = wp_slash( $postarr );

	// First, get all of the original fields.
	$post = get_post( $postarr['ID'], ARRAY_A );

	if ( is_null( $post ) ) {
		if ( $wp_error ) {
			return new WP_Error( 'invalid_post', __( 'Invalid post ID.' ) );
		return 0;

	// Escape data pulled from DB.
	$post = wp_slash( $post );

	// Passed post category list overwrites existing category list if not empty.
	if ( isset( $postarr['post_category'] ) && is_array( $postarr['post_category'] )
		&& count( $postarr['post_category'] ) > 0
	) {
		$post_cats = $postarr['post_category'];
	} else {
		$post_cats = $post['post_category'];

	// Drafts shouldn't be assigned a date unless explicitly done so by the user.
	if ( isset( $post['post_status'] )
		&& in_array( $post['post_status'], array( 'draft', 'pending', 'auto-draft' ), true )
		&& empty( $postarr['edit_date'] ) && ( '0000-00-00 00:00:00' === $post['post_date_gmt'] )
	) {
		$clear_date = true;
	} else {
		$clear_date = false;

	// Merge old and new fields with new fields overwriting old ones.
	$postarr                  = array_merge( $post, $postarr );
	$postarr['post_category'] = $post_cats;
	if ( $clear_date ) {
		$postarr['post_date']     = current_time( 'mysql' );
		$postarr['post_date_gmt'] = '';

	if ( 'attachment' === $postarr['post_type'] ) {
		return wp_insert_attachment( $postarr, false, 0, $wp_error );

	// Discard 'tags_input' parameter if it's the same as existing post tags.
	if ( isset( $postarr['tags_input'] ) && is_object_in_taxonomy( $postarr['post_type'], 'post_tag' ) ) {
		$tags      = get_the_terms( $postarr['ID'], 'post_tag' );
		$tag_names = array();

		if ( $tags && ! is_wp_error( $tags ) ) {
			$tag_names = wp_list_pluck( $tags, 'name' );

		if ( $postarr['tags_input'] === $tag_names ) {
			unset( $postarr['tags_input'] );

	return wp_insert_post( $postarr, $wp_error, $fire_after_hooks );


5.6.0Added the $fire_after_hooks parameter.
3.5.0Added the $wp_error parameter to allow a WP_Error to be returned on failure.

User Contributed Notes

  1. Skip to note 9 content


    Before calling wp_update_post() it is necessary to create an array to pass the necessary elements. Unlike wp_insert_post() , it is only necessary to pass the ID of the post to be updated and the elements to be updated. The names of the elements should match those in the database.

    // Update post 37
      $my_post = array(
          'ID'           => 37,
          'post_title'   => 'This is the post title.',
          'post_content' => 'This is the updated content.',
    // Update the post into the database
      wp_update_post( $my_post );
    Processing $wp_error

    If your updates are not working, there could be an error. It is a good idea to set $wp_error to true and display the error immediately after.

    // Of course, this should be done in an development environment only and commented out or removed after deploying to your production site.
    wp_update_post( $current_item, true );						  
    if (is_wp_error($post_id)) {
    	$errors = $post_id->get_error_messages();
    	foreach ($errors as $error) {
    		echo $error;

    Categories need to be passed as an array of integers that match the category IDs in the database. This is the case even where only one category is assigned to the post.

    Caution – Infinite loop
    When executed by an action hooked into save_post (e.g. a custom metabox), wp_update_post() has the potential to create an infinite loop. This happens because (1) wp_update_post() results in save_post being fired and (2) save_post is called twice when revisions are enabled (first when creating the revision, then when updating the original post—resulting in the creation of endless revisions).

    If you must update a post from code called by save_post, make sure to verify the post_type is not set to ‘revision’ and that the $post object does indeed need to be updated.

    Likewise, an action hooked into edit_attachment can cause an infinite loop if it contains a function call to wp_update_post passing an array parameter with a key value of “ID” and an associated value that corresponds to an Attachment.

    Note you will need to remove then add the hook, code sample modified from the API/Action reference: save_post

    function my_function( $post_id ){
    	if ( ! wp_is_post_revision( $post_id ) ){
    		// unhook this function so it doesn't loop infinitely
    		remove_action('save_post', 'my_function');
    		// update the post, which calls save_post again
    		wp_update_post( $my_args );
    		// re-hook this function
    		add_action('save_post', 'my_function');
    add_action('save_post', 'my_function');
  2. Skip to note 11 content

    Programmatically change “publish” (or “draft”) to “future” and schedule the post to be published tomorrow:

    $time = strtotime( 'tomorrow' );
    $my_post = array(
        'ID'            => 1,
        'post_status'   => 'future',
        'post_date'     => date( 'Y-m-d H:i:s', $time ),
        'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $time ),
    wp_update_post( $my_post );
  3. Skip to note 12 content

    When setting the post_date, make sure to also set the post_date_gmt.

    // Change the post date on a post with a status other than 'draft', 'pending' or 'auto-draft'
    $arg = array(
    	'ID'            => $post_id,
    	'post_date'     => $post_date,
    	'post_date_gmt' => get_gmt_from_date( $post_date ),
    wp_update_post( $arg );

    When you are setting the post date on a ‘draft’, ‘pending’ or ‘auto-draft’ post status, also set edit_date to true, otherwise the post date won’t be changed.

    // Change the post date on a post with a status 'draft', 'pending' or 'auto-draft'
    $arg = array(
    	'ID'            => $post_id,
    	'post_date'     => $post_date,
    	'post_date_gmt' => get_gmt_from_date( $post_date ),
    	'edit_date'     => true,
    wp_update_post( $arg );
  4. Skip to note 14 content

    If you’re importing and then applying some updates with this function, it doesn’t recognize ‘import_id’ so remember to also use ‘ID’ too.

    In any case, if you “import” with wp_insert_post and then use wp_update_post, you’re going to lose all your featured images and any additional categories you added between import/update will be lost. (The plugin “Featured Image From URL” solves the featured images problem, if it’s activated.)

    Also, some of the documentation suggests that you can use an author’s name or a category’s name, but as far as I can tell, you can only use the id numbers for authors and categories when importing/updating content.

  5. Skip to note 16 content
    //Example with acf https://advancedcustomfields.com
    add_action( 'acf/save_post', 'add_category_acf', 20 );
    function add_category_acf( $post_id ) {
        //uncomment if a specific post is required example post 490
        //$post_id = 490;
        $post_type = get_post_type( $post_id );
        if ( 'post' == $post_type ) {
            //add the default category with id 3
            $data = array(
                'post_category' => [3],
            wp_update_post( $data );

You must log in before being able to contribute a note or feedback.