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:
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:
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 withwp_verify_noncefor 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:
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:
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.
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 thirdIn 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:
// 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.
// 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:
// 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:
$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. Alwaysreturnthe modified data. - Using the wrong hook for the job: Don't use
initto modify post content — usethe_contentfilter. 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
$_3accidentally, 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
initbut the plugin registering it runs at a higher priority, the removal fails. Useplugins_loadedorwp_loadedto be safe.
Key takeaways
- Actions are for doing things (side effects). Use
add_actionto run your code at specific WordPress moments. - Filters are for changing data. Use
add_filterto 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_actionandremove_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.






Comments
Loading comments…