Skip to content
ExponaBLOG
WORDPRESS DEVELOPMENT & PLUGINS · DAY 004 / 100

Hooks: actions and filters — a practical WordPress guide

The foundation of WordPress development.

Expona Admin7 min read

If you've built anything meaningful in WordPress, you've relied on hooks without even thinking about it. Hooks are the invisible infrastructure that lets you inject your own code into WordPress core and third-party plugins without touching their files. Understanding hooks: actions and filters — a practical WordPress guide will transform how you approach plugin development and theme customization.

What are hooks?

Hooks are predetermined points in the WordPress execution flow where you can attach your own functions. WordPress developers call out to specific hook names at critical moments — when a post is saved, when a page is rendered, before a user is created. You write a callback function and tell WordPress to run it when that hook fires. There are two types: actions and filters, each serving a different purpose.

Think of hooks like events in a larger system. When something important happens, WordPress fires that event and says, "Is anyone listening?" If you've registered a callback for that event, your code runs. This design keeps WordPress modular and lets plugins coexist peacefully.

Actions — Run code at specific moments

Actions are hooks designed for doing things. They don't return data; they execute side effects. When you want to create something, send an email, log an event, or register a custom post type, you use an action.

Here's a practical example. You want to register a custom post type called "event" when WordPress initializes:

php
add_action('init', function () {
    register_post_type('event', [
        'public'  => true,
        'label'   => 'Events',
        'supports' => ['title', 'editor', 'thumbnail'],
    ]);
});

The init hook fires when WordPress has loaded but before the site's front end or admin is rendered. This is the correct place to register post types because WordPress needs to know about them before it starts processing requests. If you tried to register a post type later in the cycle, WordPress might not flush rewrite rules or handle queries correctly.

Let's look at a real-world scenario. You're building a plugin that tracks when posts are published. You want to send a Slack notification every time someone publishes a post:

php
add_action('publish_post', function ($post_id, $post) {
    $title = $post->post_title;
    $url = get_permalink($post_id);
    
    wp_remote_post('https://hooks.slack.com/services/YOUR/WEBHOOK', [
        'body' => json_encode([
            'text' => "New post published: {$title}",
            'url'  => $url,
        ]),
    ]);
}, 10, 2);

The publish_post hook fires whenever a post moves to "publish" status. The function receives two parameters: the post ID and the full post object. Your code sends data to Slack, and WordPress continues. No return value needed — the action just does its job.

Common action hooks you'll use:

  • init — Register post types, taxonomies, and set up plugin features.
  • wp_enqueue_scripts — Load stylesheets and JavaScript on the front end.
  • admin_enqueue_scripts — Load admin assets.
  • save_post — Run code when a post is saved (use with wp_verify_nonce for security).
  • user_register — Execute logic when a new user signs up.
  • wp_footer — Output content in the footer.

Filters — Change data before it is used

Filters are hooks designed for transformation. They take data in, modify it, and return it. Filters let you intercept and reshape data at crucial moments without changing core code.

A simple example: you want to trim whitespace from all post titles:

php
add_filter('the_title', function ($title) {
    return trim($title);
});

The the_title hook runs whenever WordPress displays a post title. Your callback receives the title, trims it, and returns the cleaned version. Every place that uses the_title now gets the trimmed result.

Let's walk through a more practical scenario. You're working on an e-commerce site and need to format product prices with a custom currency symbol and thousands separator:

php
add_filter('woocommerce_price_html', function ($price_html, $product) {
    $price = $product->get_price();
    
    if ($price) {
        $formatted = '€ ' . number_format($price, 2, ',', '.');
        return $formatted;
    }
    
    return $price_html;
}, 10, 2);

The filter receives the original price HTML and the product object. You extract the raw price, format it with your currency symbol and locale-specific thousands separator, and return the formatted string. Every place WooCommerce displays a price now uses your format. The original data is never modified in the database — only the display output changes.

Common filter hooks:

  • the_content — Modify post content before display.
  • the_excerpt — Transform excerpt text.
  • sanitize_post_meta — Clean meta values.
  • wp_kses_post_tags — Control which HTML tags are allowed in post content.
  • redirect_canonical — Override canonical URL logic.
  • query_posts_request — Modify the database query WordPress generates (advanced).

Always remember: filters must return the modified data. If you forget to return, you'll accidentally pass null down the chain and break things.

Priority — Lower numbers run first

When multiple callbacks are attached to the same hook, they run in priority order. Priority is the third parameter in add_action and add_filter. The default is 10.

php
add_filter('the_title', fn($t) => '[URGENT] ' . $t, 5);  // Runs first
add_filter('the_title', fn($t) => $t . ' (updated)', 10); // Runs second
add_filter('the_title', fn($t) => trim($t), 15);          // Runs third

In this example, a title "Hello World" becomes: "[URGENT] Hello World (updated)" (spaces trimmed). The callback with priority 5 runs first and adds the prefix. Then priority 10 adds the suffix. Finally, priority 15 trims any extra whitespace.

This matters in production. If you're using a third-party plugin that modifies content at the default priority (10), and you need to apply your transformation after theirs, use priority 15 or higher. If you need to run first, use a lower number like 5.

A real scenario: You use a SEO plugin that modifies post titles to add keywords. You want to run your brand formatting after the SEO plugin:

php
// SEO plugin uses default priority 10
// Your code runs after:
add_filter('the_title', function ($title) {
    return '🏢 ' . $title; // Brand prefix
}, 15);

The SEO plugin adds its keyword boost, then your code adds the brand prefix on top of that. Without correct priority, the order might reverse and break your intended formatting.

Remove hooks — remove_action and remove_filter

Sometimes you need to undo a hook someone else registered. WordPress provides remove_action and remove_filter for exactly this purpose.

php
// Remove a filter registered elsewhere
remove_filter('the_content', 'some_plugin_function', 10);

The parameters must match exactly: hook name, callback function name, and priority. If you get the priority wrong, the removal fails silently.

Practical use case: A plugin adds a watermark to all images in post content, but your brand guidelines forbid watermarks. You remove the filter:

php
// In your theme's functions.php or must-use plugin:
add_action('init', function () {
    remove_filter('the_content', 'watermark_plugin_add_watermark', 10);
});

You must call remove_filter after the plugin has registered its filter but before the hook fires. The init hook is safe because it runs early in WordPress execution.

For anonymous functions (callbacks defined with fn() or function() {} without a name), removal is impossible. Store the callback in a variable if you think you might need to remove it later:

php
$my_callback = function ($content) {
    return 'PREFIX: ' . $content;
};

add_filter('the_content', $my_callback, 10);

// Later, if needed:
remove_filter('the_content', $my_callback, 10);

Common mistakes

  • Forgetting to return in filters: If your filter callback doesn't return a value, it implicitly returns null, which breaks whatever depends on that data. Always return the modified data.
  • Using the wrong hook for the job: Don't use init to modify post content — use the_content filter. Wrong hooks cause race conditions and timing issues. Know your hooks.
  • Storing functions without names when you might need to remove them: If there's any chance you'll need to disable a hook callback, use a named function or store the callback in a variable. Anonymous closures can't be removed.
  • Incorrect priority guessing: If your code doesn't work in the expected order, check priorities. Trace through the plugin or theme that registered competing callbacks and use a priority that runs before or after as needed.
  • Not verifying parameters in callbacks: A hook might pass three parameters, but you only use two. If you reference $_3 accidentally, you're relying on undefined behavior. Check hook documentation or use error logging.
  • Removing hooks before they're registered: If you try to remove a filter inside init but the plugin registering it runs at a higher priority, the removal fails. Use plugins_loaded or wp_loaded to be safe.

Key takeaways

  • Actions are for doing things (side effects). Use add_action to run your code at specific WordPress moments.
  • Filters are for changing data. Use add_filter to transform data before it's used, and always return the modified value.
  • Priority controls execution order. Lower numbers run first. The default is 10.
  • You can remove hooks with remove_action and remove_filter, but only if the callback is referenceable (named function or stored variable) and you know the exact priority.
  • Hooks are the contract between WordPress core and your code. Master them, and you can safely extend WordPress without touching core files.
  • Understand which hooks fire when. Wrong hook = wrong behavior. Read documentation or trace core code to find the right moment.
  • Always return values in filters. Always verify nonces in action callbacks that handle user input.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles