Blog · Guides

Hooks and Filters: How WordPress Really Works Under the Hood

If you have ever pasted a snippet into functions.php without fully understanding why it worked, you have already used WordPress hooks. Hooks are the mechanism that lets themes, plugins, and your own code step into WordPress at precise moments and change what happens next. There are two kinds: actions and filters. Learn the difference and a lot of WordPress development suddenly makes sense.

What a Hook Actually Is

WordPress core runs through its request lifecycle (loading, querying the database, rendering the page, sending output) and at hundreds of points along the way it stops and says, in effect, “anyone want to do something here?” or “anyone want to change this value before I use it?” Those checkpoints are hooks. Plugins and themes register functions to run at specific hooks, and WordPress calls them in order when it reaches that point.

This is why WordPress is so extensible without anyone editing core files. A plugin does not need to modify wp-login.php to add a field to the login form; it just hooks into the moment the login form is rendered.

Actions vs Filters, in Plain Language

The distinction is simple once you see it:

  • Actions let you run code at a specific moment. Nothing is expected back. Think “do this thing now.” Examples: send an email when a post is published, log an event when a user logs in, enqueue a script when the page head is being built.
  • Filters let you change a value before WordPress uses it. You are handed data, and you must return data. Think “take this, modify it, hand it back.” Examples: change post content before it displays, adjust an excerpt length, alter an email subject line.

A useful mental shortcut: if the WordPress function is named do_action(), it is running your code with no return value expected. If it is apply_filters(), it is passing you a value and expecting one back.

A Real Action Example

The most common action in theme and plugin development is wp_enqueue_scripts, which fires when WordPress is ready to load styles and scripts on the front end.

function serverborn_load_assets() {
    wp_enqueue_style(
        'site-style',
        get_stylesheet_uri(),
        array(),
        '1.0'
    );
}
add_action( 'wp_enqueue_scripts', 'serverborn_load_assets' );

You are not changing any data here. You are just telling WordPress “when you get to this point, also run my function.” That is the entire pattern behind actions: register a callback with add_action(), and WordPress calls it for you at the right time.

A Real Filter Example

Filters follow the same registration pattern but your function must accept a value and return one. Here is a filter that shortens the default excerpt length:

function serverborn_custom_excerpt_length( $length ) {
    return 20;
}
add_filter( 'excerpt_length', 'serverborn_custom_excerpt_length' );

WordPress calls apply_filters( 'excerpt_length', $length ) internally when building an excerpt. Your function receives the default value, ignores it, and returns 20. If you forget to return a value from a filter callback, you will silently break the feature it controls, which is the single most common hook mistake developers make.

Priority and Arguments

Both add_action() and add_filter() accept two extra parameters: priority and the number of arguments your callback expects.

add_filter( 'the_content', 'serverborn_append_notice', 20, 1 );

function serverborn_append_notice( $content ) {
    return $content . '

Thanks for reading.

'; }

Priority defaults to 10. Lower numbers run earlier, higher numbers run later. If two plugins both hook into the_content, priority determines the order they run in, which matters if one depends on the other’s output. Adjust priority only when you have a specific reason (usually because your callback needs to run before or after another known callback).

The argument count matters when a hook passes multiple values. For example, save_post passes the post ID, the post object, and a boolean for whether it is an update:

add_action( 'save_post', 'serverborn_log_save', 10, 3 );

function serverborn_log_save( $post_id, $post, $update ) {
    if ( $update ) {
        error_log( 'Post updated: ' . $post_id );
    }
}

If you leave the argument count at its default of 1, WordPress will only give your function the first parameter, and the others simply will not arrive. This is a frequent source of “undefined variable” confusion.

Where to Put This Code

For quick, personal customizations, a child theme’s functions.php is fine. For anything you want to survive a theme switch, or anything meant to be reusable across sites, wrap it in a small custom plugin instead. A minimal plugin is just a PHP file with a header comment block placed in wp-content/plugins/. You can scaffold and manage plugins from the command line with wp-cli, which is worth learning if you maintain more than one site: wp plugin list, wp plugin activate, and similar commands make it easy to script consistent setups.

Finding the Hooks You Need

WordPress does not hide its hooks. If you want to know what fires when a page loads, when a post saves, or when a comment is submitted, the WordPress developer reference documents hook names for every core function. When working with a specific plugin, checking its source for calls to do_action() and apply_filters() is the fastest way to find integration points, since many well built plugins expose their own hooks for exactly this purpose. A caching plugin, for instance, often provides an action you can tie into so that custom code triggers a cache purge at the right moment rather than leaving stale content behind.

Common Mistakes to Avoid

  • Using add_filter() when you meant add_action(), or vice versa. Filters must return a value; actions do not.
  • Forgetting to set the argument count when your callback needs more than one parameter.
  • Hooking expensive operations (external API calls, heavy database queries) onto hooks that fire on every page load, which will slow down time to first byte regardless of how fast your server or PHP version is.
  • Naming functions generically (like init()) which can collide with other plugins. Prefix your function names.

The Takeaway

Actions let you run code at a moment in time. Filters let you change a value before WordPress uses it. Every plugin, every theme, and most of core itself is built on this pair of concepts. Once add_action() and add_filter() stop looking like magic incantations and start looking like “register a callback, WordPress calls it at the right time,” you can read almost any WordPress codebase and understand what it is doing.