WordPress Hooks Explained: What Actions and Filters Actually Do

The first time I opened a WordPress theme’s functions.php file, I saw a line like add_action('init', 'my_function'); and had no idea what it did. Nothing seemed to call my_function. It just sat there, and somehow it ran anyway. That confusion lasted embarrassingly long for me — probably three or four months of copy-pasting snippets from blogs without understanding why they worked.

Hooks are the single concept that separates “I can install plugins” from “I can build on WordPress.” Here is the explanation I wish someone had given me on day one.

A hook is just a named moment in time

Forget the word “hook” for a second. WordPress, while loading a page, runs through thousands of lines of code in a fixed order. It loads settings, then plugins, then the theme, then figures out which posts to fetch, then builds the header, then the content, then the footer.

At specific points along that journey, WordPress core stops and effectively announces: “I am about to load the theme. Does anyone want to do something right now?” That announcement is a hook. It has a name, like after_setup_theme or wp_head or save_post.

Your job is to raise your hand at the right moment. That is what add_action() does — it registers your function against a named moment. WordPress keeps a list. When that moment arrives, it runs everything on the list.

So this:

add_action('wp_footer', 'fk_add_analytics');

function fk_add_analytics() {
    echo '<!-- analytics script goes here -->';
}

…does not run your function immediately. It adds it to a queue. Later, when WordPress builds the footer, it fires do_action('wp_footer'), walks its list, and calls your function. That delay is exactly why nothing appears to “call” it.

Actions do things. Filters change things.

There are two kinds of hooks, and the difference is genuinely simple once stated plainly:

  • An action says: “here is a moment — go do something.” Send an email, log a record, print some HTML, register a post type. It does not return anything. WordPress does not care what you give back.
  • A filter says: “here is a piece of data — do you want to change it before I use it?” It hands you a value, and you must return a value. Whatever you return is what WordPress uses going forward.

The number one beginner bug — and I made it repeatedly — is forgetting to return in a filter:

// Broken. Every excerpt on the site becomes empty.
add_filter('excerpt_length', function($length) {
    $length = 25;
});

// Correct.
add_filter('excerpt_length', function($length) {
    return 25;
});

If a filter callback returns nothing, PHP returns null, and WordPress dutifully uses null. Titles vanish. Content goes blank. The site does not throw an error — it just quietly empties out. If something disappears from your site right after you add a filter, check your return statement first. It is the answer maybe 70% of the time.

Priority and arguments: the two parameters everyone ignores

Both add_action() and add_filter() accept four parameters:

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

Priority controls order. Default is 10. Lower numbers run earlier, higher numbers run later. This matters more than you would guess. If a plugin modifies the post title at priority 10 and you also modify it at priority 10, whoever registered first wins the first pass — and the other runs on the already-modified value. When your filter seems to have “no effect,” it is often because something else runs after you and overwrites your work. Bump your priority to 20 or 99 and try again. That one trick has saved me hours on client sites where a page builder was fighting my code.

Accepted args is the one that produces confusing errors. WordPress only passes your callback one argument by default, even if the hook offers three. If you write a function expecting three parameters but leave $accepted_args at its default, PHP 8 will throw an ArgumentCountError:

// Wrong: $post is never passed, fatal error on PHP 8.
add_action('save_post', 'fk_log_save');
function fk_log_save($post_id, $post) { ... }

// Right: ask for both arguments.
add_action('save_post', 'fk_log_save', 10, 2);
function fk_log_save($post_id, $post) { ... }

Whenever you copy a snippet and get an argument count error, look at that fourth number.

How to find the hook you actually need

This is the practical skill. Knowing hooks exist is easy; finding the right one out of several thousand is the real work. Three approaches, in the order I use them:

  • Search the source. If you want to change something about excerpts, grep the WordPress codebase for apply_filters near the excerpt functions. Every hook is literally defined by a do_action() or apply_filters() call in the source. Searching wp-includes/ for apply_filters( ' is a legitimate strategy and faster than guessing.
  • Use a debugging plugin. Query Monitor has a hooks panel that shows every hook fired on the current page, in order, with the callbacks attached. When I inherit a messy site, this is the first thing I install. It answers “what is modifying my title?” in about thirty seconds.
  • Read the plugin you are extending. Well-built plugins (WooCommerce is a good example) expose dozens of their own hooks. Searching the plugin folder for apply_filters is usually more productive than searching the internet, because the source is never out of date.

Why this matters beyond the syntax

Hooks are the reason you should almost never edit WordPress core or a third-party plugin directly. Every direct edit is a change that disappears on the next update — and it will disappear at the worst possible time, usually when you are on leave and someone clicks “update all.”

I have cleaned up sites where a previous developer hardcoded a change into a plugin file. It worked perfectly for eight months, then an update wiped it, and the client’s checkout broke on a Friday evening. The same change, written as a filter in a small custom plugin, would have survived every update since.

The mental shift is this: your code does not modify WordPress. Your code subscribes to WordPress and responds. That keeps your work in files you own, separate from files that update themselves.

The takeaway

If you remember four things, you can work confidently with hooks:

  • Hooks are named moments; add_action and add_filter register your code to run at one of them, later.
  • Actions do work and return nothing. Filters receive a value and must return one.
  • When your code has no effect, adjust the priority. When you get an argument error, adjust $accepted_args.
  • Put your hooks in a small custom plugin or a child theme — never in core or a vendor plugin file.

Pick one small thing on a test site — change the excerpt length, add a line to the footer, log when a post is saved — and write it as a hook today. Once the delayed-execution idea clicks, most of the WordPress codebase stops feeling like magic and starts reading like a normal application.

Faizan Khan
Faizan Khan

Technical PM & PHP developer — the manager who still ships code. 13 years turning “can we build this?” into “it’s live”.

Work with me →

Leave a comment

Enough talk. Let’s launch.

One call. An honest scope, a real timeline, and weekly updates until it ships.