WordPress Hooks – Actions and Filters Complete Guide

Published: April 10, 2026 · Author: Marcin Szewczyk-Wilgan

The WordPress hook system is the architectural foundation that makes the platform extensible. Think of hooks as electrical sockets distributed throughout the WordPress codebase – at specific points during execution, WordPress calls do_action() or apply_filters(), inviting any registered code to run or modify data at that moment. WordPress core contains over 2,500 hooks. WooCommerce adds several hundred more. Every plugin and theme that extends WordPress functionality does so primarily through hooks. Understanding how they work in detail is the single most important technical skill for WordPress development.

Actions vs Filters – The Core Distinction

There are exactly two types of hooks in WordPress. The distinction between them is precise and important:

ActionsAn action hook is a point during execution where WordPress says "something happened here – if you want to do something in response, now is the time." Actions are triggered with do_action('hook_name', $arg1, $arg2). Callbacks registered with add_action() execute at that point. Actions do not pass a value and do not need to return one. Examples: sending a notification email when a post is published, enqueuing a script on the frontend, logging a user activity.
FiltersA filter hook is a point where WordPress passes a value through registered callbacks before using it. Filters are triggered with apply_filters('hook_name', $value, $arg1). Callbacks registered with add_filter() receive the value, optionally modify it, and must return it. The value passes through each registered callback in priority order. The final return value is used by WordPress. Examples: modifying post content before display, changing the excerpt length, filtering search results.
The cardinal ruleAction callbacks do not need to return anything. Filter callbacks must always return the modified value – even if unmodified. A filter callback that does not return a value returns null, replacing the original value with null. This is one of the most common and confusing WordPress development mistakes.

add_action() and add_filter() Parameters

Signatureadd_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): true
add_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 ): true
$hook_nameThe exact name of the hook to attach to. Hook names are case-sensitive. A typo in the hook name registers the callback on a hook that never fires – this produces no error, just silent no-op behaviour that can be frustrating to debug.
$callbackAny valid PHP callable: a function name string ('my_function'), a static method (['MyClass', 'my_method']), an instance method ([$this, 'my_method'] or [$object, 'method']), or a closure (function() {}). Avoid closures for any callback that might need to be removed later – closures cannot be removed with remove_action() or remove_filter().
$priorityExecution order relative to other callbacks on the same hook. Default is 10. Lower numbers run earlier (priority 1 before priority 10 before priority 20). When two callbacks share the same priority, they run in registration order. Use priority to ensure your code runs before or after another plugin's callback on the same hook.
$accepted_argsThe number of arguments WordPress passes to your callback. Default is 1. Some hooks pass multiple arguments – save_post passes ($post_id, $post, $update). If your callback needs the second or third argument, set $accepted_args accordingly: add_action('save_post', 'my_callback', 10, 3). WordPress passes only as many arguments as this value specifies, regardless of what the hook actually provides.

Essential Action Hooks

These are the WordPress core action hooks used in the vast majority of plugins and themes:

init

After WordPress loads, before output

The most commonly used hook for plugin setup: registering custom post types, taxonomies, shortcodes, and REST API routes. Fires after WordPress has loaded but before any HTML output. Runs on both frontend and admin. Use admin_init for admin-only setup.

wp_enqueue_scripts

Enqueuing frontend assets

The correct hook for registering and enqueuing CSS and JavaScript on the frontend. Never use wp_head to output script tags directly – always use wp_enqueue_scripts with wp_enqueue_script() and wp_enqueue_style() so WordPress can manage dependencies and prevent duplicates.

save_post

Post create and update

Fires when a post is created or updated. Passes ($post_id, $post, $update). Always verify the nonce and check wp_is_post_revision() and wp_is_post_autosave() to avoid running on autosave and revision saves. Check user capabilities before modifying anything.

template_redirect

Before template loading

Fires just before WordPress determines which template to load. The correct place to redirect visitors, enforce login requirements, or perform actions based on the current page context. At this point, template tags and conditional functions like is_single(), is_page(), and is_archive() are available.

Essential Filter Hooks

The most frequently used filter hooks in WordPress plugin and theme development:

the_contentFilters post content before output. Used to add content before or after the post body, process shortcodes, transform markup. Be careful about what you add here: the filter fires on every the_content() call, including in loops and REST API responses. Always return $content.
the_titleFilters the post title. Receives ($title, $post_id). Use $accepted_args = 2 to access the post ID. Common use: appending badges, icons, or status labels to titles in specific contexts.
excerpt_length / excerpt_moreexcerpt_length filters the word count for auto-generated excerpts (default 55). excerpt_more filters the "read more" string appended after truncated excerpts (default "[…]"). Simple filters for customizing the excerpt presentation globally.
upload_mimesFilters the list of allowed file upload MIME types. Add types: $mimes['svg'] = 'image/svg+xml'; return $mimes;. Remove types by unsetting array keys. Note that adding SVG upload support also requires sanitization – never add SVG support without a sanitization plugin like Safe SVG.
login_redirectFilters the URL users are redirected to after login. Receives ($redirect_to, $requested_redirect_to, $user). Use to send different user roles to different admin screens or front-end dashboards after login.
cron_schedulesFilters the list of available WP-Cron recurrence intervals. Add custom intervals: $schedules['every_5_minutes'] = ['interval' => 300, 'display' => 'Every 5 minutes']; return $schedules;. Required before you can register a recurring cron job at custom intervals.

WooCommerce Hooks

WooCommerce adds hundreds of action and filter hooks, providing extension points throughout the store experience. Key hooks for common customizations:

Shop loops

woocommerce_before_shop_loop

Fires before the product loop on shop/archive pages. Use to add content above the product grid: filter UI, promotional banners, category descriptions. The complementary woocommerce_after_shop_loop fires after the grid closes.

Products per page

loop_shop_per_page

Filters the number of products displayed per page on shop archives. add_filter('loop_shop_per_page', function() { return 24; });. More reliable than changing the WooCommerce setting directly, as it survives setting resets and can be conditional.

Product price

woocommerce_product_get_price

Filters the raw price returned by $product->get_price(). Receives ($price, $product). Use for dynamic pricing: role-based discounts, time-limited offers, or external pricing integrations. Always return a numeric string or empty string, not null.

Order processing

woocommerce_order_status_changed

Fires when an order status changes. Receives ($order_id, $old_status, $new_status, $order). The correct hook for triggering fulfilment actions, sending custom notifications, integrating with external systems, and logging order lifecycle events.

Removing Hooks

Removing a hook added by WordPress core, a theme, or another plugin requires matching three things exactly: the hook name, the callback reference, and the priority.

Simple functionsremove_action('init', 'their_function_name', 10);
Works straightforwardly when the callback is a named function. The priority must match exactly what was used in the original add_action() call. If you are not sure of the priority, use Query Monitor's Hooks panel to inspect registered callbacks.
Class methodsremove_action('init', [$plugin_instance, 'their_method'], 10);
You need a reference to the object instance. If the plugin stores it in a global or provides an accessor (common patterns: Plugin_Name::instance(), $GLOBALS['plugin_var']), retrieve it first. Removing hooks from class instances is more reliable after the instance is created, typically in a later hook like plugins_loaded.
ClosuresClosures (anonymous functions) cannot be removed. There is no way to get a reference to a closure registered by another plugin. This is why you should never use closures for callbacks that might need to be removable by other code. Use named functions or named class methods instead.

Diagnostic Functions

did_action()did_action('init') returns the number of times the specified action has fired. Returns 0 if it has not fired yet. Use this to check whether you are calling code after or before a given hook fires – for example, checking whether wp_loaded has fired before calling certain WordPress functions.
has_filter() / has_action()has_filter('the_content', 'my_function') returns the priority at which the callback is registered, or false if it is not registered. Use to check whether a specific callback is active before attempting to remove it, or to conditionally add a callback only if another is not already registered.
current_filter() / current_action()Returns the name of the hook currently being processed, from within a callback. Useful when the same function is registered on multiple hooks and its behaviour should differ based on which hook triggered it: if (current_filter() === 'the_excerpt') { /* excerpt-specific logic */ }
Query MonitorThe Query Monitor plugin's Hooks & Actions panel shows every hook that fired on the current page request, in order, with all registered callbacks listed by priority. It is the most practical debugging tool for understanding hook execution order and identifying which plugin or theme code is running when. Essential for any serious WordPress development.

Custom Hooks and Best Practices

When building plugins or themes, define your own hooks to make your code extensible by others:

Unique prefixesAlways prefix custom hook names with your plugin or theme slug: do_action('myplugin_after_save', $data), not do_action('after_save', $data). Unprefixed hook names collide with other plugins and core hooks. The prefix makes it immediately clear which plugin defines the hook.
Document your hooksEvery custom hook in a plugin should be documented: what it does, when it fires, what arguments it passes, and what filters return. Standard format is a docblock above the do_action() or apply_filters() call. Well-documented hooks are what makes a plugin genuinely developer-friendly.
Hooks in WooCommerce integrationsWhen extending WooCommerce, prefer WooCommerce's own hooks over modifying WooCommerce templates. Template overrides break on WooCommerce updates; hooks are maintained backward compatibility through WooCommerce's deprecation process. Check the WooCommerce developer documentation for the correct hook before overriding a template.
WordPress 7.0 hooksWordPress 7.0 adds hooks for its new systems. WP AI Client provides wp_ai_client_before_prompt and wp_ai_client_after_response filters for intercepting AI prompts and responses. The Abilities API exposes hooks for registering and filtering available AI abilities. Real-Time Collaboration adds hooks for collaborator events. Review the WordPress 7.0 developer notes for the complete list.

Common Mistakes

Mistake 1

Missing return in a filter

A filter callback that performs operations but forgets return $value at the end returns null. This replaces the filtered value with null, which can break anything expecting a string, array, or object. In the the_content filter this wipes the entire post content from the page. Always return in filters.

Mistake 2

Side effects in filters

Filters should modify and return their input – they should not send emails, make database writes, or redirect as side effects. These side effects will run wherever the filter fires, including in REST API responses and background processes. Put side effects in action hooks; keep filter callbacks pure.

Mistake 3

Using anonymous closures for removable callbacks

Registering callbacks with anonymous closures makes them impossible to remove later: add_action('init', function() { /* ... */ });. Another plugin or theme cannot remove this callback. Use named functions or class methods for any callback that should be removable.

Mistake 4

Wrong $accepted_args

A callback on save_post that needs $post_id and $post but uses the default $accepted_args = 1 will only ever receive the post ID. The second argument will always be null. Specify add_action('save_post', 'cb', 10, 2) to receive both arguments.

Summary

The WordPress hook system is elegant in its simplicity: two functions to register (add_action, add_filter), two to trigger (do_action, apply_filters), and four diagnostic functions to inspect state. Everything else is application of these primitives. Mastering hooks means understanding the execution lifecycle well enough to know which hooks are available at each point, what each one passes, and how to use priority to control ordering. The mistakes to avoid are few and consistent: always return in filters, use named callbacks for removability, set $accepted_args when you need multiple arguments. With this foundation, any WordPress feature becomes approachable – because all of WordPress is built on these same mechanisms.

Need Help with WordPress?

If you have questions after reading the articles or need support – we are at your disposal. No commitments, no marketing jargon – a concrete proposal after a brief conversation.

Phone

+48 608 271 665

Mon–Fri, 8:00–16:00 CET

E-mail

contact@weboptimo.pl

We respond within 24h

Company

WebOptimo

VAT ID: PL6391758393