Book a call

WordPress Actions vs Filters: The Complete Guide with Real Examples

March 29, 2026·10 min read
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 '';
}

The add_action() function signature:

add_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 );

Priority 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
}

Accepting 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 );
    }
}

Filters: 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
}

The add_filter() function signature is identical to add_action():

add_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 );

Practical 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 . 'Written by Kamal Hosen';
}

// 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
}

Actions 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 );

Removing 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
} );

Creating 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 );

Name 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();

For 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' );

Essential 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 tag Add meta tags, inline styles
wp_footer Before closing 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

Common Mistakes to Avoid

  • Forgetting to return in a filter. The most common mistake. If your filter callback doesn’t return, the value becomes null and breaks the output silently.
  • Wrong priority when removing hooks. remove_action() must use the same priority as the original add_action() or it won’t work.
  • Hooking too early. If you call get_post() inside an init callback, the global query isn’t set up yet. Use wp or template_redirect for 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_post fires 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.