Query API Hooks
Actions and filters for modifying WordPress queries.
Source: wp-includes/class-wp-query.php, wp-includes/query.php
Actions
Query Lifecycle
pre_get_posts
Fires after the query variable object is created, but before the query is run. The primary hook for modifying queries.
do_action_ref_array( 'pre_get_posts', array( WP_Query &$query ) );
Parameters:
$query— The WP_Query instance (passed by reference)
Usage:
add_action( 'pre_get_posts', function( $query ) {
// Always check is_main_query() to avoid modifying secondary queries
if ( ! is_admin() && $query->is_main_query() ) {
// Modify home page query
if ( $query->is_home() ) {
$query->set( 'posts_per_page', 5 );
$query->set( 'cat', -10 ); // Exclude category 10
}
// Add custom post types to search
if ( $query->is_search() ) {
$query->set( 'post_type', array( 'post', 'page', 'product' ) );
}
}
} );
⚠️ Important: Use $query->is_main_query() inside this hook, not the global function is_main_query().
Since: 2.0.0
parse_query
Fires after the main query vars have been parsed.
do_action_ref_array( 'parse_query', array( WP_Query &$query ) );
Parameters:
$query— The WP_Query instance
Usage:
add_action( 'parse_query', function( $query ) {
// Query type flags are now set
if ( $query->is_category() ) {
// Modify category queries
}
} );
Since: 1.5.0
parse_tax_query
Fires after taxonomy-related query vars have been parsed.
do_action( 'parse_tax_query', WP_Query $query );
Since: 3.7.0
posts_selection
Fires to announce query’s selection parameters. For use by caching plugins.
do_action( 'posts_selection', string $selection );
Parameters:
$selection— The assembled WHERE + GROUP BY + ORDER BY + LIMIT + JOIN
Since: 2.3.0
Loop Actions
loop_start
Fires once the loop is started.
do_action_ref_array( 'loop_start', array( WP_Query &$query ) );
Since: 2.0.0
loop_end
Fires once the loop has ended.
do_action_ref_array( 'loop_end', array( WP_Query &$query ) );
Since: 2.0.0
loop_no_results
Fires if no results are found in a post query.
do_action( 'loop_no_results', WP_Query $query );
Usage:
add_action( 'loop_no_results', function( $query ) {
if ( $query->is_search() ) {
// Log failed search
error_log( 'No results for: ' . $query->get( 's' ) );
}
} );
Since: 4.9.0
the_post
Fires once the post data has been set up.
do_action_ref_array( 'the_post', array( WP_Post &$post, WP_Query &$query ) );
Parameters:
$post— The Post object$query— The WP_Query instance
Usage:
add_action( 'the_post', function( $post, $query ) {
// Runs each time a post is set up in the loop
// Good for setting up custom globals
} , 10, 2 );
Since: 2.8.0, 4.1.0 (added $query parameter)
comment_loop_start
Fires once the comment loop is started.
do_action( 'comment_loop_start' );
Since: 2.2.0
Error Handling
set_404
Fires after a 404 is triggered.
do_action_ref_array( 'set_404', array( WP_Query &$query ) );
Usage:
add_action( 'set_404', function( $query ) {
// Log 404s, redirect, etc.
} );
Since: 5.5.0
Filters
SQL Clause Filters
These filters allow modification of the SQL query. They come in two sets:
- Standard filters — For normal modifications
- Request filters (suffix
_request) — For caching plugins
posts_where
Filters the WHERE clause of the query.
apply_filters_ref_array( 'posts_where', array( string $where, WP_Query &$query ) );
Usage:
add_filter( 'posts_where', function( $where, $query ) {
global $wpdb;
if ( $query->get( 'custom_title_search' ) ) {
$search = $query->get( 'custom_title_search' );
$where .= $wpdb->prepare(
" AND {$wpdb->posts}.post_title LIKE %s",
'%' . $wpdb->esc_like( $search ) . '%'
);
}
return $where;
}, 10, 2 );
Since: 1.5.0
posts_join
Filters the JOIN clause of the query.
apply_filters_ref_array( 'posts_join', array( string $join, WP_Query &$query ) );
Usage:
add_filter( 'posts_join', function( $join, $query ) {
global $wpdb;
if ( $query->get( 'join_custom_table' ) ) {
$join .= " LEFT JOIN {$wpdb->prefix}custom_table ct ON {$wpdb->posts}.ID = ct.post_id";
}
return $join;
}, 10, 2 );
Since: 1.5.0
posts_groupby
Filters the GROUP BY clause of the query.
apply_filters_ref_array( 'posts_groupby', array( string $groupby, WP_Query &$query ) );
Since: 2.0.0
posts_orderby
Filters the ORDER BY clause of the query.
apply_filters_ref_array( 'posts_orderby', array( string $orderby, WP_Query &$query ) );
Usage:
add_filter( 'posts_orderby', function( $orderby, $query ) {
global $wpdb;
if ( $query->get( 'orderby' ) === 'title_length' ) {
$orderby = "LENGTH({$wpdb->posts}.post_title) ASC";
}
return $orderby;
}, 10, 2 );
Since: 1.5.1
posts_distinct
Filters the DISTINCT clause of the query.
apply_filters_ref_array( 'posts_distinct', array( string $distinct, WP_Query &$query ) );
Since: 2.1.0
posts_fields
Filters the SELECT clause of the query.
apply_filters_ref_array( 'posts_fields', array( string $fields, WP_Query &$query ) );
Usage:
add_filter( 'posts_fields', function( $fields, $query ) {
global $wpdb;
if ( $query->get( 'include_comment_count' ) ) {
$fields .= ", (SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_post_ID = {$wpdb->posts}.ID) as comment_count_custom";
}
return $fields;
}, 10, 2 );
Since: 2.1.0
post_limits
Filters the LIMIT clause of the query.
apply_filters_ref_array( 'post_limits', array( string $limits, WP_Query &$query ) );
Since: 2.1.0
posts_clauses
Filters all query clauses at once.
apply_filters_ref_array( 'posts_clauses', array( array $clauses, WP_Query &$query ) );
$clauses Array:
array(
'where' => string,
'groupby' => string,
'join' => string,
'orderby' => string,
'distinct' => string,
'fields' => string,
'limits' => string,
)
Usage:
add_filter( 'posts_clauses', function( $clauses, $query ) {
if ( $query->get( 'complex_modification' ) ) {
$clauses['where'] .= " AND ...";
$clauses['join'] .= " LEFT JOIN ...";
$clauses['orderby'] = "custom_order";
}
return $clauses;
}, 10, 2 );
Since: 3.1.0
Paging-Specific Filters
These fire after paging is applied:
posts_where_paged— WHERE after paging (since 1.5.0)posts_join_paged— JOIN after paging (since 1.5.0)
Request Filters (For Caching Plugins)
Identical to standard filters, for caching plugin use:
posts_where_requestposts_join_requestposts_groupby_requestposts_orderby_requestposts_distinct_requestposts_fields_requestpost_limits_requestposts_clauses_request
All since 2.5.0 or 3.1.0.
Final Query Filter
posts_request
Filters the completed SQL query before sending.
apply_filters_ref_array( 'posts_request', array( string $request, WP_Query &$query ) );
Usage:
add_filter( 'posts_request', function( $request, $query ) {
// Log all queries
error_log( $request );
return $request;
}, 10, 2 );
Since: 2.0.0
posts_request_ids
Filters the Post IDs SQL request when query is split.
apply_filters( 'posts_request_ids', string $request, WP_Query $query );
Since: 3.4.0
Results Filters
posts_pre_query
Filters the posts array before the query takes place. Return non-null to bypass WordPress queries.
apply_filters_ref_array( 'posts_pre_query', array( null $posts, WP_Query &$query ) );
Usage:
add_filter( 'posts_pre_query', function( $posts, $query ) {
// Return cached results to skip database query
$cache_key = md5( serialize( $query->query_vars ) );
$cached = wp_cache_get( $cache_key, 'custom_query_cache' );
if ( $cached !== false ) {
$query->found_posts = $cached['found_posts'];
$query->max_num_pages = $cached['max_num_pages'];
return $cached['posts'];
}
return null; // Let WordPress query
}, 10, 2 );
Since: 4.6.0
posts_results
Filters the raw post results array, prior to status checks.
apply_filters_ref_array( 'posts_results', array( WP_Post[] $posts, WP_Query &$query ) );
Since: 2.3.0
the_posts
Filters the array of posts after they’ve been fetched and processed.
apply_filters_ref_array( 'the_posts', array( WP_Post[] $posts, WP_Query &$query ) );
Usage:
add_filter( 'the_posts', function( $posts, $query ) {
if ( $query->is_main_query() && $query->is_home() ) {
// Add a synthetic "featured" post at the beginning
array_unshift( $posts, get_post( 123 ) );
}
return $posts;
}, 10, 2 );
Since: 1.5.0
found_posts
Filters the number of found posts.
apply_filters_ref_array( 'found_posts', array( int $found_posts, WP_Query &$query ) );
Usage:
add_filter( 'found_posts', function( $found_posts, $query ) {
// Adjust for pagination display
if ( $query->get( 'custom_found_posts' ) ) {
return $query->get( 'custom_found_posts' );
}
return $found_posts;
}, 10, 2 );
Since: 2.1.0
found_posts_query
Filters the query to run for retrieving found posts count.
apply_filters_ref_array( 'found_posts_query', array( string $found_posts_query, WP_Query &$query ) );
Default query: 'SELECT FOUND_ROWS()'
Since: 2.1.0
Search Filters
posts_search
Filters the search SQL in the WHERE clause.
apply_filters_ref_array( 'posts_search', array( string $search, WP_Query &$query ) );
Since: 3.0.0
posts_search_orderby
Filters the ORDER BY used for search results.
apply_filters( 'posts_search_orderby', string $search_orderby, WP_Query $query );
Since: 3.7.0
wp_search_stopwords
Filters stopwords used when parsing search terms.
apply_filters( 'wp_search_stopwords', string[] $stopwords );
Since: 3.7.0
wp_query_search_exclusion_prefix
Filters the prefix that indicates a search term should be excluded.
apply_filters( 'wp_query_search_exclusion_prefix', string $prefix );
Default: '-'
Since: 4.7.0
post_search_columns
Filters the columns to search.
apply_filters( 'post_search_columns', string[] $columns, string $search, WP_Query $query );
Supported columns: 'post_title', 'post_excerpt', 'post_content'
Since: 6.2.0
Content Filters
content_pagination
Filters the "pages" derived from splitting post content by <!-- nextpage -->.
apply_filters( 'content_pagination', string[] $pages, WP_Post $post );
Since: 4.4.0
the_preview
Filters the single post for preview mode.
apply_filters_ref_array( 'the_preview', array( WP_Post $post_preview, WP_Query &$query ) );
Since: 2.7.0
Query Splitting
split_the_query
Filters whether to split the query (fetch IDs first, then objects).
apply_filters( 'split_the_query', bool $split, WP_Query $query, string $old_request, array $clauses );
Since: 3.4.0, 6.6.0 (added $old_request and $clauses)
Attachment Queries
wp_allow_query_attachment_by_filename
Filters whether attachment queries should include filenames.
apply_filters( 'wp_allow_query_attachment_by_filename', bool $allow );
Default: false
Since: 6.0.3
Comment Feed Filters
Filters for comment feed queries:
comment_feed_join— JOIN clausecomment_feed_where— WHERE clausecomment_feed_groupby— GROUP BY clausecomment_feed_orderby— ORDER BY clausecomment_feed_limits— LIMIT clause
All since 2.2.0 or 2.8.0.
Date Query Filters
date_query_valid_columns
Filters the list of valid date query columns.
apply_filters( 'date_query_valid_columns', string[] $valid_columns );
Since: 3.7.0, 4.1.0 (added user_registered), 4.6.0 (added registered, last_updated)
get_date_sql
Filters the date query WHERE clause.
apply_filters( 'get_date_sql', string $where, WP_Date_Query $query );
Since: 3.7.0
Redirect Filters
old_slug_redirect_post_id
Filters the old slug redirect post ID.
apply_filters( 'old_slug_redirect_post_id', int $id );
Since: 4.9.3
old_slug_redirect_url
Filters the old slug redirect URL.
apply_filters( 'old_slug_redirect_url', string $link );
Since: 4.4.0
Common Patterns
Exclude Category from Home
add_action( 'pre_get_posts', function( $query ) {
if ( $query->is_home() && $query->is_main_query() ) {
$query->set( 'cat', '-5' ); // Exclude category ID 5
}
} );
Add Custom Post Type to Archives
add_action( 'pre_get_posts', function( $query ) {
if ( ! is_admin() && $query->is_main_query() ) {
if ( $query->is_category() || $query->is_tag() ) {
$query->set( 'post_type', array( 'post', 'custom_type' ) );
}
}
} );
Custom Ordering
add_filter( 'posts_orderby', function( $orderby, $query ) {
if ( $query->get( 'orderby' ) === 'random_seeded' ) {
$seed = $query->get( 'seed', 1 );
return "RAND({$seed})";
}
return $orderby;
}, 10, 2 );
Complex JOIN
add_filter( 'posts_clauses', function( $clauses, $query ) {
global $wpdb;
if ( $query->get( 'order_by_views' ) ) {
$clauses['join'] .= "
LEFT JOIN {$wpdb->postmeta} view_meta
ON {$wpdb->posts}.ID = view_meta.post_id
AND view_meta.meta_key = 'post_views'
";
$clauses['orderby'] = "CAST(view_meta.meta_value AS UNSIGNED) DESC";
}
return $clauses;
}, 10, 2 );
Query Cache Layer
add_filter( 'posts_pre_query', function( $posts, $query ) {
if ( $query->get( 'cache_this' ) ) {
$key = 'query_' . md5( serialize( $query->query_vars ) );
$cached = get_transient( $key );
if ( $cached !== false ) {
$query->found_posts = $cached['found'];
$query->max_num_pages = $cached['pages'];
return $cached['posts'];
}
}
return null;
}, 10, 2 );
add_filter( 'the_posts', function( $posts, $query ) {
if ( $query->get( 'cache_this' ) ) {
$key = 'query_' . md5( serialize( $query->query_vars ) );
set_transient( $key, array(
'posts' => wp_list_pluck( $posts, 'ID' ),
'found' => $query->found_posts,
'pages' => $query->max_num_pages,
), HOUR_IN_SECONDS );
}
return $posts;
}, 10, 2 );