Most WordPress guides have one key sentence: "use a hook". If you do not know what a hook is, that sentence sounds like a secret code. The truth is simpler. At certain points in its execution WordPress stops and asks, "Does anyone have something to say?" Every function attached to that point runs. These points are hooks.

Without hooks, you would have to edit core files to change WordPress's behavior, and everything would vanish with the first update. With hooks, your code sits next to the core. If you are just starting to write plugins, read this article together with building a WordPress plugin from scratch.

Two kinds of hooks#

ActionFilter
JobDo something at a momentChange a value
Return valueNoneYes, and it is required
Registering functionadd_actionadd_filter
Core function that runs itdo_actionapply_filters
ExamplePrint a script in the footerChange post text before display

A simple picture: an action is like a doorbell; it rings and whoever wants to can do something. A filter is like a production line; the value passes through your hands, gets changed and moves on to the next.

Your first action#

PHP
<?php
add_action( 'wp_footer', 'demo_footer_note' );

function demo_footer_note(): void {
	echo '<p class="demo-note">' . esc_html__( 'Built with WordPress', 'demo' ) . '</p>';
}

wp_footer is the theme's footer hook. Our function runs there and prints one line. Notice that we passed the output through esc_html__; always escape output at the moment you print it.

Your first filter#

PHP
<?php
add_filter( 'the_content', 'demo_append_note' );

function demo_append_note( string $content ): string {
	if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
		return $content; // Condition not met; return the original value
	}
	$note = '<p><em>' . esc_html__( 'This article is kept up to date.', 'demo' ) . '</em></p>';
	return $content . $note;
}

Three points: the function receives the value, always returns something and only changes it where needed. The in_the_loop and is_main_query conditions stop the filter from running unintentionally in widgets and sidebar content.

Priority and argument count#

The third and fourth arguments of both registering functions are priority and the number of arguments:

PHP
<?php
add_filter( 'the_title', 'demo_title_prefix', 20, 2 );

function demo_title_prefix( string $title, int $post_id ): string {
	if ( 'product' === get_post_type( $post_id ) ) {
		return '* ' . $title;
	}
	return $title;
}
  • Priority 20: runs after functions with the default priority of 10.
  • The number 2: WordPress calls this hook with two values (the title and the ID) and we want both. If you leave the number out, only the first reaches the function.

The official documentation for each hook says how many arguments it passes; do not guess.

Some commonly used hooks#

HookTypeUse
initActionRegister post types, taxonomies and shortcodes
wp_enqueue_scriptsActionLoad stylesheets and scripts on the front end
admin_menuActionAdd a page to the dashboard menu
save_postActionReact to a post being saved
the_contentFilterChange post text before display
body_classFilterAdd classes to the body tag

Check the exact name and arguments of each in the WordPress developer reference. Do not type hook names from memory, because one wrong letter means your code never runs and you see no error.

Creating custom hooks#

When you write your own plugin, let others extend it with do_action and apply_filters:

PHP
<?php
function demo_render_badge( int $user_id ): string {
	$label = apply_filters( 'demo_badge_label', __( 'Member', 'demo' ), $user_id );
	do_action( 'demo_before_badge', $user_id );

	return '<span class="badge">' . esc_html( $label ) . '</span>';
}

Anyone using your plugin can now change the label without touching your code. Prefix your hook names with the plugin's prefix so they do not collide with others.

Removing a hook#

PHP
<?php
add_action( 'init', function () {
	remove_action( 'wp_head', 'wp_generator' );
} );

To remove a hook, the function name and priority must be exactly the ones used when it was registered. Anonymous functions (closures) cannot easily be removed; if they may need removing, write a named function.

Common mistakes#

  • Not returning the value in a filter: the result is a blank page or a missing title.
  • Registering a hook too early: some functions, such as is_singular, do not give correct answers before the query is loaded. For code that depends on page state, use the wp or template_redirect hooks.
  • Using echo in a filter: a filter must return the value, not print it.
  • Forgetting to escape: escape everything you print, at the moment you print it.
  • Code in the parent theme: it disappears when you switch themes. A plugin or child theme is better.

When your code does not run properly and you get strange results, also check plugin conflicts; some plugins change the same hook with a different priority. For creating new data with the init hook, see also custom post types.

Summary#

  • A hook is a point where WordPress waits to run your functions.
  • An action does something; a filter changes a value and must return it.
  • The default priority is 10, and a lower number runs earlier.
  • Set the argument count according to that hook's documentation.
  • For your own plugin, create hooks with do_action and apply_filters.
  • Always escape output at the moment you print it, and keep your code in a plugin or child theme.