add_filter()
Attaches a callback that receives a value and must return one — the signature, the return contract, and how often filters really run.
add_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): trueadd_filter() registers a callback that receives a value and returns a replacement. The return value goes to the next callback in line, and after the last one it goes back to whatever called apply_filters(). The signature is identical to add_action(), which is unsurprising since add_action() is a wrapper around this function.
Always return something
A filter callback that returns nothing returns null, and null is what core receives. This is the most common filter bug by a wide margin, usually caused by an early bare return; guard or by a callback written as if it were an action. Every exit path must return a value, including the "not interested" path, which returns the value it was given, unchanged.
Return the right type
Core rarely coerces what you hand back. excerpt_length happens to be cast with (int), but most filters are not defended, so returning a string where an array was expected produces a fatal error far away from your code. Match the type documented on the apply_filters() call in the Code Reference.
Filters run more often than you think
the_content runs for every post in every loop on the page, plus feeds, REST responses and block-based widgets — not once per page view. Anything expensive inside multiplies, and side effects such as writing an option or sending mail will happen repeatedly.
Example
<?php
// Priority 999 so this runs after a theme's own excerpt_length filter (usually 10).
add_filter( 'excerpt_length', 'acme_excerpt_length', 999 );
function acme_excerpt_length( $length ) {
if ( is_search() ) {
return $length; // Not interested — return the value, never a bare `return;`.
}
return 25; // int: core passes this to wp_trim_words() as a word count.
}
// the_title passes two arguments: apply_filters( 'the_title', $post_title, $post_id ).
// accepted_args must be 2 to receive the second one.
add_filter( 'the_title', 'acme_flag_drafts', 10, 2 );
function acme_flag_drafts( $title, $post_id ) {
if ( ! is_admin() || 'draft' !== get_post_status( $post_id ) ) {
return $title; // Every path returns the value.
}
return $title . ' (draft)';
}When to use it
- Shorten or lengthen excerpts site-wide without touching a template.
- Append a disclaimer to the content of posts in one category.
- Change the number of posts a REST endpoint returns per page.
- Rewrite an upload's filename before WordPress stores the attachment.
More examples
The null-return bug
The failure mode to recognise on sight: titles or content disappearing on some templates but not others, with no PHP error. If a filter callback contains a bare `return;` or an `echo`, that is your bug. Actions echo, filters return.
// BROKEN: on a page, this returns null and the title vanishes site-wide.
add_filter( 'the_title', 'acme_prefix_title' );
function acme_prefix_title( $title ) {
if ( ! is_single() ) {
return; // returns null — core now has null where a string was.
}
echo 'Post: ' . $title; // echoing from a filter also prints in the wrong place
}
// FIXED.
add_filter( 'the_title', 'acme_prefix_title' );
function acme_prefix_title( $title ) {
if ( ! is_single() ) {
return $title;
}
return 'Post: ' . $title;
}__return_true and the closure it replaces
Reach for these whenever a filter just needs a fixed value and the arguments are irrelevant. Beyond being shorter, the named helpers are removable by anyone, whereas a closure that returns false is stuck on that hook for the life of the request.
// These two lines do exactly the same thing. The first is the shorthand.
add_filter( 'wp_is_application_passwords_available', '__return_false' );
add_filter( 'wp_is_application_passwords_available', function () {
return false;
} );
// WordPress ships one helper per constant return value:
// __return_true, __return_false, __return_zero, __return_null,
// __return_empty_array, __return_empty_string
// Prefer the named function: a closure cannot be removed later, but a site owner
// (or a later plugin) can always undo this:
remove_filter( 'wp_is_application_passwords_available', '__return_false' );Guarding an expensive filter
The pattern for any the_content filter that queries the database or calls an API. The context checks stop it firing in feeds and widget loops; the transient stops the work repeating; the static flag covers themes that render the content more than once on a page.
add_filter( 'the_content', 'acme_append_related', 20 ); // after wpautop (10) and do_shortcode (11)
function acme_append_related( $content ) {
// the_content runs per post, per loop, and in feeds and REST responses.
if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() || is_feed() ) {
return $content;
}
static $rendered = false;
if ( $rendered ) { // belt and braces against a template calling the_content() twice
return $content;
}
$rendered = true;
$related = get_transient( 'acme_related_' . get_the_ID() );
if ( false === $related ) {
$related = acme_build_related_html( get_the_ID() ); // the expensive part
set_transient( 'acme_related_' . get_the_ID(), $related, HOUR_IN_SECONDS );
}
return $content . $related;
}
Discussion