If you’ve been working with WordPress for more than a week, you’ve probably heard the word “hooks” thrown around. Most tutorials describe them abstractly – event-driven architecture, observer pattern, blah blah. Let me skip that and go straight to what you actually need to know to use them.
The core difference
WordPress has two types of hooks: actions and filters.
Actions let you do something at a specific point in WordPress execution. You’re not expected to return anything – you just run your code and get out. Filters let you modify a value that WordPress is about to use. You receive the value, change it however you want, and return it. That return is not optional – if you forget it, things break in weird ways.
I’ve seen junior developers use add_action where they needed add_filter and then spend an hour wondering why their changes weren’t showing up. The rule is simple: if you’re transforming data, use a filter. If you’re just reacting to an event, use an action.
add_action in practice
The most common use case is hooking into WordPress’s initialization cycle. Here’s something I use in almost every theme – enqueueing scripts and styles properly instead of hardcoding them into templates:
function mytheme_enqueue_assets() {
wp_enqueue_style(
'mytheme-main',
get_stylesheet_uri(),
array(),
'1.0.0'
);
wp_enqueue_script(
'mytheme-app',
get_template_directory_uri() . '/js/app.js',
array( 'jquery' ),
'1.0.0',
true
);
}
add_action( 'wp_enqueue_scripts', 'mytheme_enqueue_assets' );
The third argument to add_action is priority (default 10). Lower numbers run first. If you need to load something after another plugin has loaded its scripts, bump your priority to 20 or 30. I’ve had to do this with WooCommerce hooks more than once.
A second practical example – running code after a post is published:
function notify_on_publish( $new_status, $old_status, $post ) {
if ( $new_status === 'publish' && $old_status !== 'publish' ) {
// send a Slack notification, clear a cache, whatever
do_something_with( $post->ID );
}
}
add_action( 'transition_post_status', 'notify_on_publish', 10, 3 );
Note the fourth argument: 3. That tells WordPress to pass 3 arguments to your callback. The default is 1, and if you forget to specify this when your hook passes multiple arguments, you’ll only get the first one.
add_filter in practice
Filters are where things get interesting. One of the most-used filters is the_content, which runs on post content before it’s displayed:
function append_reading_time( $content ) {
if ( ! is_single() ) {
return $content;
}
$word_count = str_word_count( strip_tags( $content ) );
$minutes = max( 1, ceil( $word_count / 200 ) );
$label = $minutes === 1 ? '1 min read' : $minutes . ' min read';
$suffix = '<p class="reading-time">' . esc_html( $label ) . '</p>';
return $content . $suffix;
}
add_filter( 'the_content', 'append_reading_time' );
I’d recommend testing this kind of thing in your local WordPress dev environment first – filters on the_content can interact badly with page builders and block editor plugins if you’re not careful about the context check.
Another filter I use often is login_redirect. By default WordPress dumps everyone on the dashboard after login. This changes it based on role:
function custom_login_redirect( $redirect_to, $request, $user ) {
if ( isset( $user->roles ) && is_array( $user->roles ) ) {
if ( in_array( 'subscriber', $user->roles, true ) ) {
return home_url( '/my-account/' );
}
}
return $redirect_to;
}
add_filter( 'login_redirect', 'custom_login_redirect', 10, 3 );
Creating your own hooks
You’re not limited to hooks WordPress provides. In plugins or themes with multiple components, defining your own hooks is a clean way to let other code extend your functionality without editing your files directly. Use do_action() to fire an action and apply_filters() to create a filter point:
// In your plugin, fire an action at a logical point
do_action( 'myplugin_after_process', $result, $user_id );
// Create a filter so output can be modified externally
$output = apply_filters( 'myplugin_output_html', $default_html, $context );
This pattern is worth learning early – it’s how good plugins stay extensible without turning into spaghetti.
A few things that trip people up
- Not returning in a filter callback. If you add a filter and forget
return $value, WordPress getsnullback. The content disappears or the option resets to empty. Always return. - Wrong argument count. When a hook passes 2+ arguments, you must set the fourth parameter of
add_action/add_filterto match. Default is 1. - Hooking too late. Some hooks like
initfire early in the request cycle. If you’re registering post types or taxonomies, you need to be there. Hooking intowp_loadedor later won’t work for those. - Naming collisions. If you name your callback function something generic like
enqueue_scripts, another plugin might define the same function and cause a fatal error. Prefix everything with your theme or plugin slug.
The WordPress developer docs on hooks are actually quite good and list all the core hooks by category. Once you have the mental model down, that reference is all you need.
Removing hooks
One more thing worth knowing: you can remove hooks with remove_action and remove_filter. This is useful when a plugin or theme attaches a callback you want to override or disable entirely. The catch is timing – you have to call remove_action after the original add_action has run, but before the hook fires. Getting the priority right matters here too. If the original was added at priority 10, you have to remove it at priority 10 as well, otherwise WordPress won’t find it to remove.
Hooks are probably the single most important concept to internalize when working with WordPress. Everything else – themes, plugins, the block editor’s server-side rendering – builds on top of this system. Get comfortable with them early and the rest of WordPress development becomes a lot more predictable.