wp_after_insert_post and WordPress 6.5 Hook Changes

WordPress 6.5 refined the post-save hook sequence and introduced changes to wp_after_insert_post, the Interactivity API hooks, and the new template_include improvements. Understanding hook order is critical to avoid infinite loops and double-processing.

Problem: Code hooked into save_post reads post meta or taxonomy terms that are saved in the same request — but because save_post fires before meta and terms are committed, the hook reads stale data.

Solution: Switch to wp_after_insert_post (introduced in WordPress 5.6) — it fires after meta, terms, and taxonomies have all been saved in the same request, so your hook always reads the final committed state. Pass $update as the second parameter to distinguish inserts from updates.

The examples below demonstrate correct usage of wp_after_insert_post for post-save side effects, the wp_after_insert_post $update parameter to distinguish creates from updates, and new 6.5 filter hooks.

// wp_after_insert_post fires AFTER all meta and terms are saved
// (unlike save_post which fires before meta is fully committed)
// Signature: do_action( 'wp_after_insert_post', $post_id, $post, $update, $post_before )

add_action( 'wp_after_insert_post', function( int $post_id, WP_Post $post, bool $update, ?WP_Post $post_before ) {
    // Skip auto-saves, revisions, and non-published posts
    if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) return;
    if ( 'publish' !== $post->post_status ) return;

    if ( ! $update ) {
        // New post — trigger one-time actions (e.g., send welcome notification)
        myplugin_notify_subscribers( $post );
    } else {
        // Update — check if the post was previously a draft
        if ( $post_before && 'draft' === $post_before->post_status ) {
            myplugin_notify_subscribers( $post ); // first publish from draft
        }
    }

    // Safe to read meta here — it's fully committed
    $featured_image_id = get_post_thumbnail_id( $post_id );
    if ( $featured_image_id ) {
        myplugin_generate_og_image( $featured_image_id );
    }
}, 10, 4 );

// WordPress 6.5: new filter to modify the_content lazily (applied during REST render)
add_filter( 'render_block_core/paragraph', function( string $block_content, array $block ) {
    // Modify only paragraphs inside a Query Loop
    if ( ! empty( $block['attrs']['isQueryLoop'] ) ) {
        return '
' . $block_content . '
'; } return $block_content; }, 10, 2 );

Post save hook order reference:

// WordPress post save hook execution order:
// 1. pre_post_update          (before DB write, update only)
// 2. wp_insert_post_data      (filter — modify data before DB write)
// 3. wp_before_insert_post    (action — just before INSERT/UPDATE)
// 4. DB write happens
// 5. save_post_{post_type}    (action — post is in DB, meta not yet saved)
// 6. save_post                (action)
// 7. wp_insert_post           (action)
// 8. [meta boxes / REST meta saved here]
// 9. wp_after_insert_post     (action — EVERYTHING is committed) ← safest hook

// Prevent infinite loop when updating post from within a hook:
add_action( 'wp_after_insert_post', function( $post_id, $post ) {
    // Remove the action before programmatic update, re-add after
    remove_action( 'wp_after_insert_post', __FUNCTION__, 10 );
    wp_update_post( [ 'ID' => $post_id, 'post_excerpt' => wp_trim_words( $post->post_content, 30 ) ] );
    add_action( 'wp_after_insert_post', __FUNCTION__, 10, 2 );
}, 10, 2 );

NOTE: Use wp_after_insert_post instead of save_post any time your hook reads or writes post meta — it's the only hook where meta is guaranteed to be fully committed to the database.

Leave Comment

Your email address will not be published. Required fields are marked *