Type something to search...
What are WordPress hooks, actions and filters?

What are WordPress hooks, actions and filters?

WordPress hooks are points in the WordPress code where you can "hook in" your own functions to change how WordPress behaves, without editing core files. There are two kinds: actions, which let you run code at a specific moment (like when a post is published), and filters, which let you modify data before WordPress uses or displays it (like changing the length of an excerpt). Nearly every theme and plugin relies on hooks to do its job.

Once you understand hooks, the rest of WordPress development starts to make sense. This article explains what hooks are, how actions and filters differ, how priorities and arguments work, which hooks you will use most often, and how to create and remove hooks in your own code.

What Are WordPress Hooks?

A hook is a named event inside WordPress. When WordPress reaches that point while loading a page, it checks whether any functions have been attached to that name and runs them. This is the foundation of the WordPress Plugin API.

Think of hooks as sockets built into the walls of a house. WordPress provides the sockets in hundreds of places. You bring the appliance (your function) and plug it in. You never have to rewire the house.

This design matters for one big reason: you should never edit WordPress core files. Any changes would be wiped out at the next update. Hooks give you a supported, update-safe way to customize everything from the login screen to the post content.

Why Are Hooks Important?

Hooks let you:

  • Add features without modifying WordPress core, themes, or other plugins.
  • Keep your customizations safe through updates.
  • Change the output of other plugins and themes when they provide hooks.
  • Organize your code into small, focused functions.
  • Let other developers extend your own themes and plugins.

Actions vs. Filters: The Key Difference

Both actions and filters use the same underlying system, but they serve different purposes.

Actions: Do Something

An action runs your function at a certain moment. It does not need to return anything. You use actions to send an email, enqueue a stylesheet, register a post type, or print extra HTML into the page.

You attach a function to an action with add_action():

function sajjad_add_footer_note() {
echo '<p class="footer-note">' . esc_html__( 'Thanks for visiting!', 'sajjad' ) . '</p>';
}
add_action( 'wp_footer', 'sajjad_add_footer_note' );

WordPress fires the wp_footer action near the end of every front-end page, so this message now appears on every page.

Filters: Change Something

A filter receives a value, lets you modify it, and must return the value. You use filters to change text, adjust query settings, alter HTML output, or modify arrays of options.

You attach a function to a filter with add_filter():

function sajjad_custom_excerpt_length( $length ) {
return 30;
}
add_filter( 'excerpt_length', 'sajjad_custom_excerpt_length' );

WordPress passes the default excerpt length (55 words) to your function, and whatever you return becomes the new value.

A Simple Way to Remember

  • Action: "When this happens, also do this."
  • Filter: "Before you use this value, let me change it."

If you forget to return a value from a filter, the data becomes null and things break. For example, forgetting the return in a the_content filter will make every post look empty. That is the most common beginner mistake with hooks.

How Hooks Work Behind the Scenes

For every hook, WordPress keeps a list of attached callbacks. Two core functions trigger them:

  1. do_action( 'hook_name', $arg1, $arg2 ): Runs every function attached to an action. Return values are ignored.
  2. apply_filters( 'hook_name', $value, $arg1 ): Passes $value through every attached function in order, and returns the final result.

For example, somewhere in core WordPress does something like this when it prints post content:

$content = apply_filters( 'the_content', $content );
echo $content;

Every function hooked to the_content gets a chance to modify the content before it is displayed.

Understanding Priority and Accepted Arguments

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

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

Priority

Priority is an integer that controls the order in which callbacks run. Lower numbers run first. The default is 10. If two functions share the same priority, they run in the order they were added.

add_action( 'wp_head', 'sajjad_first', 1 );
add_action( 'wp_head', 'sajjad_last', 99 );

Priority is useful when you need to run after another plugin has done its work, or before it.

Accepted Arguments

Many hooks pass more than one value. By default, your callback only receives the first one. To receive more, set $accepted_args:

function sajjad_notify_on_publish( $new_status, $old_status, $post ) {
if ( 'publish' !== $new_status || 'publish' === $old_status ) {
return;
}

if ( 'post' !== $post->post_type ) {
return;
}

wp_mail(
get_option( 'admin_email' ),
sprintf( 'New post published: %s', $post->post_title ),
get_permalink( $post )
);
}
add_action( 'transition_post_status', 'sajjad_notify_on_publish', 10, 3 );

The transition_post_status action passes three arguments, so we set $accepted_args to 3.

Commonly Used Action Hooks

These are the actions you will reach for most often:

  • init: Fires after WordPress has loaded but before headers are sent. Use it to register post types, taxonomies, and shortcodes.
  • wp_enqueue_scripts: The correct place to load CSS and JavaScript on the front end.
  • admin_enqueue_scripts: Load CSS and JavaScript in the admin area.
  • after_setup_theme: Register theme supports, menus, and image sizes.
  • wp_head and wp_footer: Output code into the <head> or before </body>.
  • save_post: Runs whenever a post is created or updated.
  • admin_menu: Add pages to the WordPress admin menu.
  • rest_api_init: Register custom REST API routes.
  • template_redirect: Runs before WordPress chooses a template, useful for redirects.

Example: Enqueueing a Stylesheet

function sajjad_enqueue_assets() {
wp_enqueue_style(
'sajjad-custom',
get_stylesheet_directory_uri() . '/assets/custom.css',
array(),
'1.0.0'
);
}
add_action( 'wp_enqueue_scripts', 'sajjad_enqueue_assets' );

Example: Redirecting Logged-Out Users

function sajjad_protect_members_page() {
if ( is_page( 'members' ) && ! is_user_logged_in() ) {
wp_safe_redirect( wp_login_url( get_permalink() ) );
exit;
}
}
add_action( 'template_redirect', 'sajjad_protect_members_page' );

Commonly Used Filter Hooks

  • the_content: Modify post content before it is displayed.
  • the_title: Modify post titles.
  • excerpt_length and excerpt_more: Control excerpts.
  • body_class: Add CSS classes to the <body> tag.
  • upload_mimes: Control which file types can be uploaded.
  • login_redirect: Change where users go after logging in.
  • wp_nav_menu_items: Add items to navigation menus.
  • document_title_parts: Modify the page title shown in the browser tab.

Example: Adding Content After Every Post

function sajjad_append_cta( $content ) {
if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
return $content;
}

$cta = '<div class="post-cta"><p>' . esc_html__( 'Enjoyed this article? Subscribe for more.', 'sajjad' ) . '</p></div>';

return $content . $cta;
}
add_filter( 'the_content', 'sajjad_append_cta' );

The checks ensure the message only appears on single posts in the main content, not in widgets, excerpts, or other places where the_content might run.

Example: Adding a Body Class

function sajjad_body_classes( $classes ) {
if ( is_user_logged_in() ) {
$classes[] = 'is-member';
}
return $classes;
}
add_filter( 'body_class', 'sajjad_body_classes' );

Example: Changing the Login Redirect

function sajjad_login_redirect( $redirect_to, $requested_redirect_to, $user ) {
if ( $user instanceof WP_User && ! user_can( $user, 'edit_posts' ) ) {
return home_url( '/account/' );
}
return $redirect_to;
}
add_filter( 'login_redirect', 'sajjad_login_redirect', 10, 3 );

Where to Put Hook Code

You have three main options:

  1. Child theme functions.php: Good for design-related changes tied to your theme. If you switch themes, the code stops running.
  2. A custom plugin: Best for functionality you want to keep regardless of theme, such as custom post types or email notifications.
  3. A code snippets plugin: Tools such as Code Snippets or WPCode let you add small hook snippets from the admin without touching files.

Always take a backup before adding code to a live site, and test on a staging copy when possible. A single syntax error in functions.php can take the site offline.

Using Anonymous Functions and Class Methods

Callbacks do not have to be named functions. You can use closures:

add_filter(
'excerpt_more',
function () {
return '&hellip;';
}
);

Closures are convenient, but they have one downside: they cannot easily be removed later with remove_filter(), because you have no reference to them. For shared or distributed code, prefer named functions or class methods.

Inside a class, pass an array with the object and method name:

class Sajjad_Tweaks {

public function __construct() {
add_action( 'init', array( $this, 'register_shortcodes' ) );
add_filter( 'the_title', array( $this, 'mark_featured' ), 10, 2 );
}

public function register_shortcodes() {
add_shortcode( 'year', array( $this, 'current_year' ) );
}

public function current_year() {
return esc_html( wp_date( 'Y' ) );
}

public function mark_featured( $title, $post_id = 0 ) {
if ( $post_id && get_post_meta( $post_id, 'featured', true ) ) {
return $title . ' *';
}
return $title;
}
}

new Sajjad_Tweaks();

For static methods, use array( 'Class_Name', 'method_name' ) or the string 'Class_Name::method_name'.

How to Remove a Hook

Sometimes a theme or plugin adds something you do not want. You can detach it with remove_action() or remove_filter(). The hook name, callback, and priority must match exactly how it was added.

function sajjad_remove_emoji_scripts() {
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
remove_action( 'wp_print_styles', 'print_emoji_styles' );
}
add_action( 'init', 'sajjad_remove_emoji_scripts' );

Timing matters. You must call remove_action() after the original add_action() has run, otherwise there is nothing to remove yet. That is why removals are often wrapped in a later hook such as init, after_setup_theme, or wp_loaded.

If a plugin attaches a method from an object instance, you need access to that same instance. Well-written plugins usually expose it through a global or a function such as Plugin_Name::instance().

How to Create Your Own Custom Hooks

If you build themes or plugins, adding your own hooks lets other developers extend your work without editing it. It is the same technique WordPress core uses.

Custom Action

function sajjad_render_pricing_table() {
echo '<div class="pricing-table">';

do_action( 'sajjad_before_pricing_rows' );

echo '<p>' . esc_html__( 'Basic plan', 'sajjad' ) . '</p>';

do_action( 'sajjad_after_pricing_rows' );

echo '</div>';
}

Another developer can now inject content:

add_action(
'sajjad_after_pricing_rows',
function () {
echo '<p class="note">' . esc_html__( 'All prices include tax.', 'sajjad' ) . '</p>';
}
);

Custom Filter

function sajjad_get_button_label() {
$label = __( 'Buy now', 'sajjad' );
return apply_filters( 'sajjad_button_label', $label );
}

And someone can change the label:

add_filter(
'sajjad_button_label',
function ( $label ) {
return __( 'Get started', 'sajjad' );
}
);

Always prefix custom hook names with your plugin or theme slug so they never collide with other code.

Useful Helper Functions for Hooks

WordPress includes a handful of functions that help you inspect and control hooks:

  • has_action() / has_filter(): Check if a callback is attached.
  • did_action( 'init' ): Returns how many times an action has fired, useful to check whether a hook has already run.
  • doing_action() / doing_filter(): Check whether a hook is currently running.
  • current_filter(): Returns the name of the hook currently being processed.
  • remove_all_filters(): Removes every callback from a hook. Use with care.

To see which hooks run on a page, the free Query Monitor plugin shows fired hooks and the callbacks attached to them, which makes debugging much easier.

Common Mistakes to Avoid

Forgetting to return in a filter: Always return a value, even if you did not change it.

Hooking too early or too late: Registering a post type on wp_head is too late. Use the hook designed for the job, such as init.

Echoing inside a filter: Filters should return strings, not print them. Echoing in the_content puts your output in the wrong place.

Running heavy code on every page: Hooks like init fire on every request, including admin and AJAX. Add conditions so expensive code only runs where it is needed.

Mismatched priority in removals: remove_action() only works if the priority matches the one used in add_action().


FAQ: WordPress Hooks, Actions and Filters

An action runs code at a specific moment and does not need to return anything. A filter receives a value, modifies it, and must return it so WordPress can continue using it.

Put theme-related changes in your child theme's functions.php file, and site functionality in a custom plugin or a snippets plugin so it survives theme changes.

Priority controls the order callbacks run on the same hook. Lower numbers run earlier, and the default is 10.

Usually the priority does not match, or your removal runs before the original add_action call. Wrap your removal in a later hook and use the same priority as the original.

The WordPress Developer Resources site documents every core hook. The Query Monitor plugin also shows hooks fired on the current page, and searching plugin code for do_action and apply_filters reveals plugin hooks.

Yes. PHP hooks still run with block themes, and filters like render_block let you modify block output. The block editor also has JavaScript hooks through the @wordpress/hooks package.

The hook system itself is very fast. Performance depends on what your callbacks do, so avoid heavy database queries or remote requests on hooks that fire on every page load.


Conclusion

Hooks are the heart of WordPress extensibility. Actions let you run code at just the right moment, and filters let you reshape data before WordPress uses it. Together they allow themes and plugins to change almost anything about a site without touching core files, which keeps your work safe through every update.

Start small: add a filter to tweak your excerpt length, or an action to enqueue a stylesheet. As you grow more confident, explore priorities, multiple arguments, and your own custom hooks. Keep Query Monitor handy, always return values from filters, and you will find that hooks make WordPress development far more predictable and enjoyable.

Tags :
Share :

Related Posts

What are the best WordPress security plugins?

What are the best WordPress security plugins?

The best WordPress security plugins for most sites are Wordfence, Sucuri Security, Solid Security, MalCare, All-In-One Security (AIOS), Patchstack, a

Dive Deeper
What is the difference between posts and pages in WordPress?

What is the difference between posts and pages in WordPress?

The main difference between posts and pages in WordPress is that posts are timely, dated entries that appear in your blog feed, archives, and RSS fee

Dive Deeper
How much does it cost to build a WordPress website?

How much does it cost to build a WordPress website?

Building a WordPress website can cost anywhere from roughly the price of a domain and a year of budget hosting, if you do it yourself with free tools

Dive Deeper