WordPress Actions vs Filters: The Complete Guide with Real Examples

What Are WordPress Hooks?
WordPress hooks are the backbone of every plugin and theme. They let you run your own code at specific points in WordPress’s execution cycle — without touching core files. There are two types: actions and filters. Understanding the difference is the single most important concept in WordPress plugin development.
The short version: an action does something at a point in time. A filter modifies something and returns the modified value. Every hook in WordPress is one or the other.
Actions: Running Code at the Right Moment
An action hook fires at a specific point — when a post is saved, when the page header loads, when a user logs in. You attach your function to it with add_action(). Your function runs, does its work, and returns nothing.
// Basic action: add something to the footer
add_action( 'wp_footer', 'my_plugin_footer_script' );
function my_plugin_footer_script() {
echo '<script>console.log("Plugin loaded");</script>';
}
PHPThe add_action() function signature:
add_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 );
PHPPriority controls execution order when multiple callbacks are attached to the same hook. Lower number = runs earlier. Default is 10. If two callbacks have the same priority, they run in the order they were registered.
// Runs before most other callbacks on init
add_action( 'init', 'my_early_init', 1 );
// Runs after most other callbacks on init
add_action( 'init', 'my_late_init', 99 );
function my_early_init() {
// fires first
}
function my_late_init() {
// fires last
}
PHPAccepting Arguments in Actions
Some action hooks pass data to your callback. You need to declare how many arguments you want to receive using the fourth parameter of add_action():
// The save_post hook passes 3 arguments: $post_id, $post, $update
add_action( 'save_post', 'my_save_post_handler', 10, 3 );
function my_save_post_handler( int $post_id, WP_Post $post, bool $update ) {
if ( $update ) {
// Post is being updated, not created
error_log( 'Post updated: ' . $post->post_title );
}
}
PHPFilters: Modifying Data Before It’s Used
A filter hook intercepts a value, lets you modify it, and expects you to return something back. The key rule: always return a value in a filter callback. If you forget to return, the filtered value becomes null and you’ll break things silently.
// Filter: modify the excerpt length
add_filter( 'excerpt_length', 'my_custom_excerpt_length' );
function my_custom_excerpt_length( int $length ): int {
return 30; // Return 30 words instead of the default 55
}
PHPThe add_filter() function signature is identical to add_action():
add_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 );
PHPPractical Filter Examples
// Modify post content before display
add_filter( 'the_content', 'my_append_signature' );
function my_append_signature( string $content ): string {
if ( ! is_single() ) {
return $content; // Don't modify on archive pages
}
return $content . '<p class="author-sig">Written by Kamal Hosen</p>';
}
// Modify the login error message (hide which field is wrong for security)
add_filter( 'login_errors', 'my_generic_login_error' );
function my_generic_login_error( string $error ): string {
return 'Invalid credentials. Please try again.';
}
// Change the number of posts per page on a specific post type archive
add_filter( 'pre_get_posts', 'my_portfolio_posts_per_page' );
function my_portfolio_posts_per_page( WP_Query $query ): void {
if ( ! is_admin() && $query->is_main_query() && $query->is_post_type_archive( 'portfolio' ) ) {
$query->set( 'posts_per_page', 12 );
}
// Note: pre_get_posts is an action that receives the query by reference,
// but it's also used as a filter pattern — no return needed here
}
PHPActions vs Filters: Side-by-Side
| Actions | Filters | |
|---|---|---|
| Purpose | Run code at a point in time | Modify a value before it’s used |
| Return value | Not required (ignored) | Required — always return something |
| Function | add_action() | add_filter() |
| Triggered by | do_action() | apply_filters() |
| Common examples | init, wp_head, save_post | the_content, the_title, excerpt_length |
| Side effects | Expected (echoing, DB writes) | Should be avoided |
Removing Hooks
You can remove any action or filter that another plugin or theme registered — as long as you know the exact callback name and priority.
// Remove a named function hook
remove_action( 'wp_head', 'wp_generator' ); // Remove WP version from head
remove_filter( 'the_content', 'wpautop' ); // Remove auto paragraph wrapping
// The priority must match what was used in add_action/add_filter
// Default priority is 10 — if the original used a different priority, match it:
remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 );
PHPRemoving hooks from class methods is slightly different — you need a reference to the object:
// If a plugin registered a hook like this:
// add_action( 'init', [ $this, 'setup' ] );
// You need the object instance to remove it
global $my_plugin_instance;
remove_action( 'init', [ $my_plugin_instance, 'setup' ] );
// Anonymous functions CANNOT be removed — avoid them for public hooks
// This cannot be undone by other code:
add_action( 'init', function() {
// anonymous — irremovable
} );
PHPCreating Your Own Hooks
If you’re building a plugin for others to extend, you should add your own hooks. This is what makes plugins like WooCommerce so extensible — they fire hundreds of custom actions and filters that other developers hook into.
// In your plugin — fire a custom action
function my_plugin_process_order( int $order_id ): void {
// ... do processing ...
// Let other code run after order processing
do_action( 'my_plugin_after_order_processed', $order_id );
}
// In your plugin — apply a custom filter
function my_plugin_get_price( float $base_price, int $product_id ): float {
// Allow other code to modify the price
return apply_filters( 'my_plugin_product_price', $base_price, $product_id );
}
// Another plugin/theme can now hook into yours:
add_action( 'my_plugin_after_order_processed', function( int $order_id ) {
// Send a Slack notification, update a CRM, etc.
} );
add_filter( 'my_plugin_product_price', function( float $price, int $product_id ): float {
return $price * 0.9; // Apply 10% discount
}, 10, 2 );
PHPName your hooks with a unique prefix matching your plugin slug — myplugin_ — to avoid collisions with WordPress core or other plugins.
Using Hooks Inside Classes
Most modern plugins use OOP. Registering hooks inside a class is straightforward with [ $this, 'method_name' ]:
class My_Plugin {
public function __construct() {
add_action( 'init', [ $this, 'register_post_types' ] );
add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ] );
add_filter( 'the_content', [ $this, 'filter_content' ] );
}
public function register_post_types(): void {
// register_post_type() calls here
}
public function enqueue_assets(): void {
wp_enqueue_style( 'my-plugin', plugin_dir_url( __FILE__ ) . 'assets/style.css' );
}
public function filter_content( string $content ): string {
// modify and return
return $content;
}
}
// Instantiate once
new My_Plugin();
PHPFor static methods, use the class name string instead of $this:
add_action( 'init', [ 'My_Plugin', 'static_method' ] );
// or shorthand:
add_action( 'init', 'My_Plugin::static_method' );
PHPEssential Action Hooks Every Developer Should Know
| Hook | When it fires | Common use |
|---|---|---|
init | After WordPress loads, before headers sent | Register CPTs, taxonomies, rewrite rules |
wp_enqueue_scripts | Front-end asset loading | Enqueue CSS/JS for themes and plugins |
admin_enqueue_scripts | Admin asset loading | Enqueue admin-only CSS/JS |
wp_head | Inside <head> tag | Add meta tags, inline styles |
wp_footer | Before closing </body> | Add inline scripts, tracking code |
save_post | After a post is saved | Save custom meta, trigger notifications |
admin_menu | Admin menu is built | Register admin pages and subpages |
plugins_loaded | All plugins are loaded | Plugin compatibility checks |
template_redirect | Before template loads | Redirects, access control |
wp_ajax_{action} | Ajax request (logged in) | Handle admin-side AJAX |
wp_ajax_nopriv_{action} | Ajax request (public) | Handle front-end AJAX |
Essential Filter Hooks Every Developer Should Know
| Hook | Filters | Common use |
|---|---|---|
the_content | Post content before display | Append/prepend content, shortcode processing |
the_title | Post title | Add prefixes, icons, labels |
excerpt_length | Excerpt word count | Change default 55-word limit |
excerpt_more | Excerpt trailing text | Change “[…]” to a Read More link |
body_class | Array of body CSS classes | Add conditional classes |
upload_mimes | Allowed upload file types | Add SVG, custom file types |
wp_mail | Email args before sending | Change from address, add CC/BCC |
login_redirect | URL after login | Role-based login redirects |
cron_schedules | WP-Cron intervals | Add custom cron intervals |
Debugging Hooks
When hooks aren’t firing as expected, these techniques help:
// Check if a function is hooked to an action
if ( has_action( 'init', 'my_function' ) ) {
echo 'my_function is hooked to init';
}
// Get the priority it's registered at
$priority = has_filter( 'the_content', 'my_content_filter' );
// Returns false if not hooked, or the priority integer if it is
// Check what's hooked to a given action (inspect the global)
global $wp_filter;
var_dump( $wp_filter['init'] ); // Shows all callbacks and priorities
// The best approach: install Query Monitor plugin
// It shows all hooks fired on the current request, their callbacks, and priorities
PHPCommon Mistakes to Avoid
- Forgetting to return in a filter. The most common mistake. If your filter callback doesn’t return, the value becomes
nulland breaks the output silently. - Wrong priority when removing hooks.
remove_action()must use the same priority as the originaladd_action()or it won’t work. - Hooking too early. If you call
get_post()inside aninitcallback, the global query isn’t set up yet. Usewportemplate_redirectfor anything that needs query data. - Using anonymous functions for public hooks. You can’t remove an anonymous function later. Use named functions or class methods for any hook other code might need to unhook.
- Running heavy code on every hook fire.
save_postfires multiple times per save (once for the post, once for each revision). Always add a guard:if ( wp_is_post_revision( $post_id ) ) return;
Frequently Asked Questions
What is the difference between an action and a filter in WordPress?
An action runs your code at a specific point in WordPress’s execution and returns nothing. A filter intercepts a value, lets you modify it, and must return something. Use add_action() when you want to do something; use add_filter() when you want to change something.
Can I use add_filter() for action hooks?
Technically yes — WordPress uses the same internal registry for both. But you should use the semantically correct function: add_action() for actions and add_filter() for filters. It makes your code readable and is expected by other developers.
What does hook priority mean in WordPress?
Priority is an integer (default 10) that controls the order callbacks run when multiple functions are attached to the same hook. Lower numbers run first. If two callbacks share the same priority, they run in registration order. Use a higher priority (e.g. 99) to run after core and other plugins, or a lower priority (e.g. 1) to run before them.
How do I find all hooks available in WordPress?
The WordPress Developer Reference lists all core hooks. The Query Monitor plugin shows every hook fired on any given page load, including which callbacks are attached. For WooCommerce hooks specifically, the WooCommerce hooks documentation is the best reference.
When should I create my own hooks in a plugin?
Add your own hooks wherever another developer might reasonably want to extend or modify your plugin’s behaviour — before and after major operations, and around any value that should be customisable. This is what makes your plugin “developer-friendly” and is standard practice for any plugin you plan to sell or distribute.