Himanshu SharmaSoftware engineer
Let’s talk

WordPress · Lesson 28 of 45

Hooks: actions and filters

How WordPress hooks work, with add_action(), add_filter(), do_action() and apply_filters(), priorities, arguments, removing hooks and creating your own.

15 minHooks and pluginsUpdated 23 October 2026

By the end of this lessonChange WordPress behaviour without editing core or other people's code, using actions to run code and filters to change values.

The idea of hooks

WordPress, its themes and plugins are full of named points where other code can join in. These points are called hooks. You never edit WordPress core to change something; you attach a function ("callback") to a hook. There are two kinds:

  • Actions say "something is happening now". Your function runs and does something: enqueue a script, send an email, add a column. It returns nothing.
  • Filters say "here is a value, change it if you want". Your function receives the value and returns the new one: a title, a list of CSS classes, an excerpt length.

Everything else in the Hooks and plugins section builds on this lesson.

Your first action

Hooks need a place to live. For small experiments, use a must-use plugin: any PHP file in wp-content/mu-plugins/ loads automatically on every request, with nothing to activate. (The next lesson makes a normal plugin.)

php
<?php
// Runs when WordPress prints the footer of the front end.
add_action( 'wp_footer', function () {
	echo '<p class="footer-note">Thanks for visiting.</p>';
} );
http
GET /
result
Thanks for visiting.

wp_footer is an action WordPress fires when it prints the end of the page. Adding a callback made it run there. The first argument of add_action() is the hook name, the second is the callback: an anonymous function as here, or the name of a function as a string.

Your first filter

A filter gets a value and must return one. This one changes the "Read more" text on excerpts:

php
<?php
add_filter( 'excerpt_more', function ( string $more ): string {
	return ' [read on]';
} );

add_filter( 'the_title', function ( string $title, int $post_id ): string {
	return $post_id === 1 ? $title . ' (welcome!)' : $title;
}, 10, 2 );
php
<?php
echo get_the_title( 1 ), "\n";
echo apply_filters( 'excerpt_more', ' [...]' ), "\n";
result
Hello world! (welcome!)
 [read on]

Two things to learn here:

  • A filter must return. If the callback returned nothing, the title would become empty.
  • The fourth argument of add_filter() (here 2) says how many arguments your callback accepts. The the_title filter passes the title and the post ID; by default your function would receive only the first.

Priority and order

Several callbacks can attach to one hook. Their order is set by the priority, the third argument (default 10). Lower numbers run first; equal priorities run in the order they were added.

php
<?php
add_filter( 'my_text', fn( $t ) => $t . ' B', 20 );
add_filter( 'my_text', fn( $t ) => $t . ' A', 5 );
add_filter( 'my_text', fn( $t ) => $t . ' C' );     // default 10

echo apply_filters( 'my_text', 'start' ), "\n";
result
start A C B

Use a high number (such as 99) to run after other code, or a low one to run before. You need this to override a theme or plugin that sets the same thing.

Making your own hooks

You can add hooks to your own code so that others (or your future self) can extend it without editing it:

php
<?php
function shop_price_label( float $price ): string {
	/**
	 * Filters the text shown for a price.
	 *
	 * @param string $label The formatted price.
	 * @param float  $price The raw price.
	 */
	$label = apply_filters( 'shop_price_label', '£' . number_format( $price, 2 ), $price );

	do_action( 'shop_price_shown', $price );   // an action: "this happened"
	return $label;
}

add_filter( 'shop_price_label', fn( $label, $price ) => $price == 0 ? 'Free' : $label, 10, 2 );
add_action( 'shop_price_shown', fn( $price ) => print( "price shown: $price\n" ) );

echo shop_price_label( 4.5 ), "\n";
echo shop_price_label( 0 ), "\n";
result
price shown: 4.5
£4.50
price shown: 0
Free

apply_filters( 'hook', $value, ...$extra ) runs the callbacks and returns the final value. do_action( 'hook', ...$args ) runs the callbacks and returns nothing. Prefix your hook names with your plugin name (shop_) so they never clash.

Removing and checking hooks

php
<?php
function add_exclamation( $t ) { return $t . '!'; }
add_filter( 'my_text', 'add_exclamation' );
echo has_filter( 'my_text', 'add_exclamation' ) ? "attached\n" : "not attached\n";

remove_filter( 'my_text', 'add_exclamation' );
echo has_filter( 'my_text', 'add_exclamation' ) ? "attached\n" : "not attached\n";
result
attached
not attached

To remove a hook you must use the same name, callback and priority that were used to add it. That is why an anonymous function is hard to remove: keep important callbacks as named functions or class methods. Other helpers: did_action( 'init' ) tells you how often an action has fired, and current_filter() names the hook that is running.

Hooks you will use constantly

Hook Type Fires
init action after WordPress has loaded; register post types and taxonomies here
wp_enqueue_scripts action when front-end assets should be added
after_setup_theme action when a theme sets up features
save_post action after a post is saved
admin_menu action when the admin menu is built
the_content filter on a post's content before it is shown
the_title filter on a post's title
body_class filter on the list of classes for <body>

The full list lives in the WordPress developer reference. The most useful habit is to search it for the hook you need, then read what arguments it passes.

Common mistakes

  • Forgetting to return in a filter.
  • Echoing in a filter instead of returning.
  • Not accepting enough arguments: add the count as the fourth argument.
  • Hooking too early or too late: get_the_title() before init works, but registering a post type after init does not.
  • Anonymous callbacks you later need to remove.

Exercise

Add a filter on body_class that adds the class has-hooks to every page, then check the <body> tag.

Show one possible answer
php
add_filter( 'body_class', function ( array $classes ): array {
	$classes[] = 'has-hooks';
	return $classes;
} );

body_class passes an array of classes; add one and return the array.

Key points

  • Hooks are named points in WordPress where your code can run (actions) or change a value (filters).
  • add_action( 'hook', $callback ) runs your function at that point; add_filter( 'hook', $callback ) receives a value and must return it.
  • The priority (default 10) sets the order, the last argument sets how many values your function receives.
  • Plugins and themes can offer their own hooks with do_action() and apply_filters(), and remove others' with remove_action() and remove_filter().

Check yourself

1. What must a filter callback always do?
2. What is the difference between an action and a filter?
3. Two callbacks are added to the same hook with priorities 5 and 20. Which runs first?
4. Which function lets you create a hook other developers can use?