Skip to content
WordPress

WordPress Actions and Filters: How Hooks Work in Practice

WordPress hooks and filters are the extension mechanism that makes all customisation possible. This guide covers actions vs filters, priorities, removal, creating custom hooks, and debugging.

WordPress Actions and Filters: How Hooks Work in Practice

Every time a WordPress plugin changes menu item positions, a caching plugin clears the cache on post publish, or a theme modifies the login page, WordPress hooks and filters are doing the work. Hooks are the extension mechanism that makes WordPress endlessly customisable — they allow code to be injected at specific points in WordPress execution without modifying core files, and they are the correct way to extend WordPress theme and plugin functionality in a maintainable, upgrade-safe way. For the bigger picture, our Complete Guide to WordPress How pulls everything together.

WordPress Hooks and Filters — Actions vs Filters

WordPress hooks and filters are two related but distinct systems. Actions are hooks where code runs at a specific point in WordPress execution, optionally producing side effects (sending emails, updating the database, enqueueing scripts). Filters are hooks where data passes through a function that can inspect and modify it before returning it for use.

Actions use add_action($hook, $callback, $priority, $accepted_args) to register a callback that runs when do_action($hook) fires. The callback returns nothing (return values are ignored). Common actions: init fires after WordPress initialises but before headers are sent — the correct place to register post types, taxonomies, and rewrite rules. wp_enqueue_scripts fires when front-end scripts and styles should be loaded. admin_init fires in the admin after WordPress initialises. save_post fires after a post is saved — used to trigger additional processing, send notifications, or update related data.

Filters in WordPress hooks and filters use add_filter($hook, $callback, $priority, $accepted_args). The callback must return a value — the modified (or unmodified) version of the data passed to it. Returning nothing or returning null replaces the data with null, breaking anything that expected the original value. Common filters: the_content allows modifying post content before it outputs. the_title allows modifying post titles. wp_nav_menu_items allows modifying navigation menu HTML. login_redirect controls where users go after logging in. According to the WordPress developer documentation, the priority parameter (default 10) controls the execution order among multiple callbacks on the same hook — lower numbers run first, higher numbers run later. Passing a higher priority (e.g., 999) to a filter that overrides an existing filter ensures it runs after the original and gets the final say on the value.

Common WordPress Hooks and Filters Patterns

Practical WordPress hooks and filters usage covers recurring patterns that appear in almost every WordPress theme and plugin. Mastering these patterns enables most common customisation needs without writing complex code from scratch.

Modify post content using the_content filter — add an author bio after every post:

add_filter('the_content', function($content) {
    if (!is_single() || !in_the_loop()) {
        return $content; // Only apply to single posts, in the main loop
    }
    $author_id = get_the_author_meta('ID');
    $bio = '<div class="author-bio"><p>' . 
           esc_html(get_the_author_meta('description', $author_id)) . 
           '</p></div>';
    return $content . $bio;
});

The is_single() and in_the_loop() checks prevent the bio from appearing in unexpected contexts where the_content filter also fires — excerpts, RSS feeds, and search results all pass through this filter. Always add context guards to content filters to prevent unintended output. Run code after post save using the save_post action:

add_action('save_post', function($post_id) {
    // Prevent running on autosaves
    if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
    // Prevent running on post revisions
    if (wp_is_post_revision($post_id)) return;
    // Custom processing here
    update_post_meta($post_id, '_last_processed', time());
}, 10, 1);

These guard clauses prevent WordPress hooks and filters callbacks from running in contexts where they would produce incorrect results or create infinite loops — save_post fires on every post save including autosaves, revisions, and programmatic saves, so checking DOING_AUTOSAVE and wp_is_post_revision is essential for save callbacks that should only run on intentional manual saves.

WordPress Hooks and Filters Priority and Removal

Priority management in WordPress hooks and filters controls which callbacks run first and enables one piece of code to override another’s output — the core mechanism for theme children overriding parent functions and plugins overriding core WordPress behaviour.

Default priority is 10. Any callback added at priority 11 or higher runs after all priority-10 callbacks. This is how a child theme overrides a parent theme’s filter: the parent adds a filter at priority 10, the child adds a replacement at priority 11 — the child’s version processes the data after the parent, effectively overriding the parent’s output. WordPress core hooks run at various priorities; hooks with explicit priority documentation in the developer reference should be checked before choosing a priority number that might conflict.

Remove existing hooks using remove_action() and remove_filter(): remove_action('wp_head', 'wp_generator'); removes the WordPress version from the HTML head. remove_filter('the_content', 'wpautop'); disables the automatic paragraph wrapping. Removal must specify the exact hook, callback, and priority used in the original add_action/add_filter call — removing at priority 10 does not remove a hook added at priority 5. For hooks added by plugins with anonymous functions (common in newer plugin code), removal is not possible without knowing the internal callback reference. This is why plugin developers should use named functions or class methods for callbacks that other code might need to remove, rather than anonymous functions. Our guide on fixing WordPress admin bar missing shows a real-world example where WordPress hooks and filters — specifically the show_admin_bar filter — are used to control admin bar visibility, and how removing or modifying that filter restores the expected behaviour.

Custom Hooks — Creating Your Own

Creating custom WordPress hooks and filters makes themes and plugins extensible allows other code to extend functionality without modifying the original source. This is the pattern that makes WordPress plugins extensible — well-written plugins provide their own hooks so third-party code can modify their behaviour.

Create a custom action in a plugin or theme function:

function mytheme_render_product_card($product_id) {
    do_action('mytheme_before_product_card', $product_id);
    echo '<div class="product-card">';
    // Card HTML here
    do_action('mytheme_inside_product_card', $product_id);
    echo '</div>';
    do_action('mytheme_after_product_card', $product_id);
}

Create a custom filter that allows modification of generated output:

function mytheme_get_product_title($product_id) {
    $title = get_the_title($product_id);
    return apply_filters('mytheme_product_title', $title, $product_id);
}

Any code can now hook into these custom WordPress hooks and filters: add_filter('mytheme_product_title', function($title, $id) { return '[SALE] ' . $title; }, 10, 2); — this adds a sale prefix to all product titles without touching the theme’s source code. This design pattern from WordPress hooks and filters — creating hooks around all modifiable outputs and at all significant execution points — is the hallmark of well-architected WordPress development. WordPress core itself follows this pattern extensively, which is why so many aspects of WordPress behaviour can be customised through hooks without core file modification. Reviews from the WordPress plugin developer handbook confirm that providing well-documented hooks in plugins is both a best practice requirement for WordPress.org directory listing and the primary mechanism by which the WordPress plugin ecosystem achieves the interoperability that makes complex multi-plugin sites function correctly.

Debugging WordPress Hooks and Filters

Diagnosing problems with WordPress hooks and filters — a filter not firing, a callback running at the wrong priority, or unexpected output from a hook — requires visibility into which hooks are registered and in which order they execute. Standard PHP debugging tools provide limited visibility here; WordPress-specific tools provide much more.

Query Monitor’s “Hooks & Actions” panel shows every hook that fired during the current request, along with all callbacks registered on each hook, their priority, and their source file. This panel is invaluable for verifying that a hook registered in functions.php is actually firing, identifying conflicts between plugins that register incompatible callbacks on the same hook, and confirming the order of execution for priority-dependent filter chains. To use: install Query Monitor → enable the debug bar display → load any WordPress page → click the Query Monitor toolbar item → navigate to Hooks & Actions. Search for any hook name to see all callbacks registered on it.

A quick debugging technique for filter chains: temporarily add a callback at a very high priority (9999) that logs the data value at that point in the chain, confirming what the filter produced before and after specific callbacks run:

add_filter('the_content', function($content) {
    error_log('Content at 9999: ' . substr(strip_tags($content), 0, 100));
    return $content;
}, 9999);

This logs the first 100 characters of the content at the end of all filter processing — compare with an identical log at priority 1 to see the net effect of all the_content filters. Remove this debugging code before deploying to production. For WordPress hooks and filters that should fire but do not, verify the hook name matches exactly (hooks are case-sensitive), the add_action/add_filter call runs before the hook fires (adding a filter after do_action/apply_filters has already run has no effect on that request), and that DOING_AUTOSAVE or similar guards are not preventing the callback from executing in the current context. Our guide on using WordPress site health covers the debug mode configuration that complements Query Monitor for identifying PHP errors produced by incorrectly coded hook callbacks.

Late binding for WordPress hooks and filters using object-oriented PHP requires understanding when the object is instantiated relative to when the hook fires. Adding a method callback with add_action('init', [$this, 'my_method']); inside a class constructor requires the object to be created before the init hook fires — typically by instantiating the class on a hook that fires earlier, like plugins_loaded. A common mistake: calling new MyPlugin(); on a hook that fires after the hooks the class’s constructor registers. The result is a hook that appears to be registered but never fires because the registration happened after the hook already fired on that request. Always instantiate plugin classes on plugins_loaded or earlier to ensure all WordPress hooks and filters the class registers are added before the hooks they target fire.

WordPress provides has_filter($hook, $callback) and has_action($hook, $callback) to check whether a specific callback is registered on a hook — useful for conditional logic that should only run if another plugin’s hook is active, or for debugging whether a particular callback has been registered. did_action($hook) returns the number of times a specific action has fired — useful for verifying that an action ran, or for preventing a callback from running more than once if it is somehow called in a context where the action fires multiple times. These utility functions make WordPress hooks and filters code more robust and self-documenting — instead of relying on implicit execution order assumptions, they explicitly check hook state before taking action that depends on it.

Performance considerations for WordPress hooks and filters matter on hooks that fire on every request, particularly filters applied to all posts on archive pages. A the_content filter that runs a database query on every post in an archive loop runs that query once per post — on a page showing 10 posts, that is 10 additional queries per request. Cache the query result using a transient or object cache, or restructure the code to batch the queries before the loop rather than inside it. Profiling WordPress hooks and filters performance with Query Monitor shows which filters add the most execution time — the “Hooks & Actions” panel shows the number of callbacks per hook and Query Monitor’s DB Queries panel attributes slow queries to the specific hook callback that triggered them, providing actionable data for optimising hook-heavy filter chains.

REST API hooks extend WordPress hooks and filters to the REST API endpoints, allowing modification of API responses, authentication, and query parameters without modifying core REST API code. rest_prepare_{post_type} filters allow adding or modifying fields in the REST API response for any post type: add_filter('rest_prepare_post', function($response, $post, $request) { $response->data['custom_field'] = get_post_meta($post->ID, 'custom_field', true); return $response; }, 10, 3); — this adds a custom field to every post object returned by the REST API. rest_pre_get_posts and similar hooks allow modifying REST API queries before execution. For the block editor, which communicates exclusively through the REST API, these REST API WordPress hooks and filters are the correct extension points for adding block editor features that depend on custom post data — modifying the REST API response is always preferable to using custom shortcodes or PHP-rendered content for features the block editor needs to interact with.

Multisite-specific WordPress hooks and filters fire on network-wide events and provide network-level extension points. wpmu_new_blog fires when a new subsite is created — use it to automatically set up the new site with default content, activated plugins, or initial settings. delete_blog fires before a subsite is deleted. network_admin_menu fires to add items to the network admin sidebar. These network-level hooks are only available in multisite installations and are the correct mechanism for plugins that need to respond to or control subsite lifecycle events across the entire WordPress multisite network. Our guide on setting up WordPress multisite covers the network architecture that provides the context for these multisite-specific WordPress hooks and filters, including when to use network actions versus subsite-specific actions for features that need to span the entire multisite installation.

Nikolas Lamprou

Nikolas Lamprou (MSc; GCFR, SC-200, Security+) has been working with computers professionally since 2009 — starting with web development and e-commerce, and moving into cybersecurity over the years. Based in Greece, he brings over 15 years of real-world IT experience to SolveTechToday, where he writes about Windows fixes, software reviews, security tools, and AI applications. His goal is straightforward: cut through the noise and give readers clear, honest guidance on the tech decisions that matter.

Stay Ahead

Fix your next problem before it starts

Get the week's best Windows fixes, software picks, and security guides delivered straight to your inbox. No noise, just solutions.

Press ESC to close · Try "Windows 11" or "Chrome"