WordPress Action vs Filter Hooks: How They Work, With Examples
Quick Summary
An action hook runs your function at a point in WordPress and ignores its return value; a filter hook passes a value through your function and uses what it returns.
A filter callback must always return the value, or the value becomes null; set priority and accepted_args when you need to run later or receive more than one argument.
remove_action() and remove_filter() only work with the same hook, callback and priority, and only after the callback has been added.
A hook is a named point in WordPress where your own code can run. An action hook runs your function at that moment so it can do something, like print HTML or save data, and WordPress ignores what the function returns. A filter hook passes a value through your function and uses whatever you return, so a filter can change the value and must always return it. You attach code with add_action() or add_filter(); WordPress (or a theme or plugin) fires the hook with do_action() or apply_filters().
My short notes on it: hooks are WordPress’s event broadcasting system. There are two types, action hooks and filter hooks. A filter can change the final value; an action can’t hand a value back. Each hook callback has a priority, and a lower number runs earlier. The rest of this post explains each of those with code. Everything below was checked against the WordPress 7.1.2 source (wp-includes/plugin.php and class-wp-hook.php) and run in memory with WordPress loaded.
Action hooks: add_action() and do_action()
do_action( 'hook_name', $arg1, $arg2... ) is the broadcast: “this is happening now”. add_action( 'hook_name', 'your_function' ) is the subscription: “when it happens, call my function”.
The most common use is printing something in a spot a theme or WordPress already provides. This goes in your (child) theme’s functions.php or a small plugin:
PHP
function mytheme_footer_note() {
echo '<p class="footer-note">Thanks for reading.</p>';
}
add_action( 'wp_footer', 'mytheme_footer_note' );
WordPress calls do_action( 'wp_footer' ) inside wp_footer(), which a classic theme’s footer.php calls just before
. Your function runs there and prints the paragraph.
You can create your own action hooks the same way. Anything you pass after the hook name reaches the callback as arguments:
Run with WordPress 7.1.2 loaded, this printed Order 42 saved, Order 43 saved and then 2.
“An action can’t modify the result” needs one note. An action returns nothing to WordPress, but it can still change things as a side effect. pre_get_posts is an action, and it passes the WP_Query object itself, so $query->set( 'posts_per_page', 5 ) inside it changes the query. The difference is that nothing is handed back: an action changes things by acting on them, a filter by returning a new value.
Filter hooks: add_filter() and apply_filters()
apply_filters( 'hook_name', $value, $arg1... ) sends a value through every callback attached to the hook and returns the final result. Each callback receives the current value and returns the value for the next one.
WordPress trims automatic excerpts to 55 words, then runs the number through the excerpt_length filter. To use 20 words:
With that code loaded, apply_filters( 'excerpt_length', 55 ) returned int(20).
A filter must return a value
This is the most common filter bug. WordPress uses the return value of your callback as the new value, whatever it is. If you change the variable but forget return, PHP returns null, and the value is gone:
PHP
function mytheme_add_mark( $title ) {
$title .= '!';
// No return: the filter now hands back null.
}
add_filter( 'mytheme_title', 'mytheme_add_mark' );
var_dump( apply_filters( 'mytheme_title', 'Hello' ) ); // NULL
That printed NULL. On a real hook such as the_title or the_content, the text simply disappears from the page. Always return the value, also on the paths where you decide not to change it.
Actions vs filters at a glance
Action: attach with add_action(), fire with do_action(). The callback does something (echo, save, send, enqueue). Its return value is ignored.
Filter: attach with add_filter(), fire with apply_filters(). The callback receives a value and must return it, changed or not. It shouldn’t echo anything.
Under the hood they are the same system. In plugin.php, add_action() is a one-line wrapper that calls add_filter(), and both kinds of hook are stored in the same $wp_filter array of WP_Hook objects. What makes a hook an action or a filter is how it is fired.
Hook priority
The third argument of add_action() and add_filter() is the priority. The default is 10. A lower number runs earlier, a higher number runs later, and callbacks with the same priority run in the order they were added.
Although they were added in the order third, second, first, wp_footer printed first, second, third. Use priority when your code has to run before or after someone else’s on the same hook: a later priority on a filter gets the last word on the value, and an earlier priority on an action runs before the default callbacks at 10.
accepted_args: getting more than one argument
The fourth argument says how many arguments your callback receives. The default is 1. Many hooks pass more. the_title, for example, is fired as apply_filters( 'the_title', $post_title, $post_id ), but your callback only gets the post ID if you ask for 2:
If you leave out 10, 2, WordPress passes only the first argument. In my test, a callback with a default value for its second parameter got the default (missing) instead of the real argument; without a default, PHP throws an ArgumentCountError for the missing argument. So when a callback needs a second argument, set the priority and the argument count together.
the_title runs wherever WordPress prints a title, including menu items, where the second argument is the menu item’s ID. That’s why the example checks the post type instead of changing every title.
Removing a hook: remove_action() and remove_filter()
remove_action( $hook, $callback, $priority ) and remove_filter() take a callback off a hook, for example to stop a plugin or parent theme printing something. They return true if something was removed and false if it wasn’t found. Three things must match what was added:
The same hook name.
The same priority. WordPress stores callbacks per priority, and remove_action() only looks at the priority you pass (default 10). Core adds the emoji script with add_action( 'wp_head', 'print_emoji_detection_script', 7 ), so removing it at the default priority does nothing. In my test, remove_action( 'wp_head', 'print_emoji_detection_script' ) returned false and the version below returned true.
The same callback. A function name is easy. For a class method, the callback is the object plus the method name, and it must be the same object instance: removing with array( new Some_Class(), 'method' ) returned false, while the original instance returned true. An anonymous function can’t be removed unless you kept it in a variable: removing an identical-looking closure returned false and the filter kept running.
Timing matters too. You can only remove a callback after it has been added; removing first returns false and the later add_action() puts it back. A child theme’s functions.php loads before the parent theme’s, so remove parent-theme callbacks from a hook that runs after both files load, such as after_setup_theme. Use the priority the parent theme used (20 in this made-up example):
If a plugin adds its callback inside a hook such as init, remove it on a hook that fires after that, or on the same hook with a later priority. To find the name and priority, look for the add_action() line in the plugin or theme.
Checking hooks: has_action(), has_filter() and did_action()
has_action( 'hook' ) returns true if anything is attached. has_action( 'hook', 'callback' ) returns the callback’s priority (an integer) or false. Because priority 0 is a valid answer, test it with false !== has_action( ... ), as the core docs advise. Since WordPress 6.9 you can pass a priority as a third argument, and it then returns true or false for that priority.
has_filter() works the same way for filters (it’s the same function underneath).
did_action( 'hook' ) returns how many times an action has fired so far, 0 if never. It’s a simple way to check whether you’re running too early or too late, for example if ( did_action( 'init' ) ). did_filter() does the same for filters (WordPress 6.1+).
current_filter() gives the name of the hook that is running right now, useful when one callback is attached to several hooks.