Skip to content

WordPress Hooks — Actions and Filters Complete Developer Guide

DodaTech Updated 2026-06-27 13 min read

In this tutorial, you'll learn the WordPress hooks system — how actions and filters let you modify and extend WordPress behavior without editing core files, with practical examples for theme and plugin development.

What You'll Learn

  • What hooks are and how they form WordPress's event system
  • The difference between actions (do something) and filters (modify something)
  • How add_action and add_filter register your callbacks
  • How the priority system determines execution order
  • How to remove hooks with remove_action and remove_filter
  • The most common action and filter hooks for theme and plugin development
  • How to create your own custom hooks with do_action and apply_filters
  • How to check if hooks exist with has_action and has_filter
  • Best practices for naming, documentation, and hook placement

Why It Matters

Imagine you buy a house but cannot change anything inside. You cannot paint the walls, move the furniture, or install new light fixtures. WordPress without hooks is exactly this — a fixed system you cannot extend. Hooks are the electrical outlets of WordPress. They let you plug in your own code at specific points without touching the original wiring. Every WordPress plugin you use depends on hooks. Every theme customization relies on them. Without hooks, WordPress would be a static product rather than an extensible platform.

Real-World Use

A membership site needs to: redirect non-logged-in users from premium content pages, add a custom field to user profiles, send an email when a user updates their password, and modify the login page logo. Every single one of these tasks uses hooks. The redirect uses template_redirect action. The profile field uses show_user_profile action. The password email uses wp_login action. The logo uses login_headerurl filter. No core files are edited. All changes stay in the theme's functions.php or a custom plugin.

Learning Path

flowchart LR
    A[WP_Query & The Loop] --> B["Hooks
You are here"]:::current B --> C[Shortcodes] C --> D[REST API] classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px

What Are Hooks?

A hook is a specific point in WordPress execution where you can run your own code. {{< ilink "WordPress" }} has two types of hooks:

  • Actions — run your code at a specific moment (e.g., after a post is saved, when the admin menu loads)
  • Filters — modify data before it is used (e.g., change the post content, modify the excerpt length)

Think of actions as doorbells. WordPress rings the doorbell at certain moments, and your code answers. Filters are like conveyor belts. Data passes along the belt, your code inspects and modifies it, then the modified data continues to its destination.

Actions

Actions fire at specific moments in WordPress execution. Your add_action call tells WordPress: "When this moment happens, run my function."

function dodatech_send_new_post_notification( $post_id ) {
    $post = get_post( $post_id );
    if ( $post->post_type !== 'post' ) return;

    $to = get_option( 'admin_email' );
    $subject = 'New post published: ' . $post->post_title;
    $message = 'A new post has been published: ' . get_permalink( $post_id );

    wp_mail( $to, $subject, $message );
}
add_action( 'publish_post', 'dodatech_send_new_post_notification' );

Every time a post transitions to "published" status, WordPress fires the publish_post action, passing the post ID to any registered callbacks. Our function receives that ID and sends an email.

Filters

Filters receive data, let you modify it, and expect you to return it:

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

function dodatech_excerpt_more( $more ) {
    return '... continue reading';
}
add_filter( 'excerpt_more', 'dodatech_excerpt_more' );

The excerpt_length filter passes the current word count (default 55). Our callback returns 30. The excerpt_more filter passes the default "[...]" string. We replace it with custom text. Filters always receive the data as the first parameter and must return it after modification.

The add_action / add_filter Signature

add_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 );
add_filter( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 );

Hook Name

The name of the hook you are attaching to. WordPress core provides hundreds. Themes and plugins can register their own.

Callback

The function to run when the hook fires. Use a string for a named function, an array for a class method:

// Named function
add_action( 'init', 'dodatech_custom_function' );

// Class method (static)
add_action( 'init', array( 'MyClass', 'static_method' ) );

// Class method (instance)
add_action( 'init', array( $this, 'instance_method' ) );

// Anonymous function (PHP 5.3+, avoid for removability)
add_action( 'init', function() {
    // code here
} );

Priority

Priority determines execution order. Lower numbers run first. Default is 10:

add_action( 'init', 'dodatech_runs_first', 5 );
add_action( 'init', 'dodatech_runs_third', 15 );
add_action( 'init', 'dodatech_runs_second', 10 );

If two callbacks share the same priority, they execute in the order they were registered.

Accepted Arguments

Some hooks pass more than one argument. You must tell WordPress how many your callback expects:

function dodatech_comment_moderation( $comment_id, $comment_approved, $commentdata ) {
    // uses three parameters
}
add_action( 'comment_post', 'dodatech_comment_moderation', 10, 3 );

The comment_post action passes three arguments: comment ID, approval status, and comment data array. Without 3 as the fourth parameter, WordPress only passes the first argument.

Removing Hooks

Use remove_action() and remove_filter() to unhook previously registered callbacks:

// Remove a filter added by another plugin
remove_filter( 'the_content', 'wpautop' );

// Remove with matching priority
remove_action( 'wp_head', 'print_emoji_detection_script', 7 );

// Remove class method
remove_action( 'admin_menu', array( 'Some_Plugin_Class', 'add_menu_page' ) );

You need the exact same function name, priority, and number of arguments used to add the hook. To remove hooks added by classes you do not control, use the $wp_filter global or a debugging plugin.

Common Action Hooks

Hook When It Fires Common Use
init After WordPress loads, before headers sent Register CPTs, taxonomies, shortcodes
wp_enqueue_scripts On front-end, for enqueuing assets Add CSS and JS files
admin_menu When building admin menu Add admin pages and submenus
admin_enqueue_scripts In admin, for admin assets Admin-specific CSS/JS
save_post After a post is saved Save custom meta box data
wp_head Inside tag Add meta tags, analytics
wp_footer Before Add footer scripts
widgets_init When registering widgets Register widget areas
admin_notices In admin area for messages Show admin notices
login_enqueue_scripts On login page Customize login page
template_redirect Before template is determined Redirect unauthorized users

Practical: Enqueue Scripts

function dodatech_enqueue_assets() {
    wp_enqueue_style(
        'dodatech-style',
        get_template_directory_uri() . '/assets/css/style.css',
        array(),
        '1.0.0'
    );

    wp_enqueue_script(
        'dodatech-script',
        get_template_directory_uri() . '/assets/js/main.js',
        array( 'jquery' ),
        '1.0.0',
        true
    );

    wp_localize_script( 'dodatech-script', 'dodatech_ajax', array(
        'ajax_url' => admin_url( 'admin-ajax.php' ),
        'nonce'    => wp_create_nonce( 'dodatech_nonce' ),
    ) );
}
add_action( 'wp_enqueue_scripts', 'dodatech_enqueue_assets' );

Practical: Admin Menu

function dodatech_add_admin_menu() {
    add_menu_page(
        'DodaTech Settings',
        'DodaTech',
        'manage_options',
        'dodatech',
        'dodatech_settings_page',
        'dashicons-admin-generic',
        30
    );
    add_submenu_page(
        'dodatech',
        'DodaTech Help',
        'Help',
        'manage_options',
        'dodatech-help',
        'dodatech_help_page'
    );
}
add_action( 'admin_menu', 'dodatech_add_admin_menu' );

Common Filter Hooks

Filter What It Modifies Common Use
the_content Post content before display Add social sharing buttons, ads
the_title Post title Modify or truncate titles
excerpt_length Excerpt word count Change excerpt length
excerpt_more Excerpt suffix Change "[...]" text
body_class CSS classes on Add custom classes
upload_mimes Allowed file types Add SVG, WebP upload support
wp_mail_from Email sender address Change from email
wp_mail_from_name Email sender name Change from name
nav_menu_css_class Menu item CSS classes Add active/focused classes
wp_nav_menu_items Menu HTML items Add custom menu items
post_thumbnail_html Featured image HTML Lazy load, wrap in div

Practical: Allow SVG Uploads

function dodatech_allow_svg( $mimes ) {
    $mimes['svg'] = 'image/svg+xml';
    return $mimes;
}
add_filter( 'upload_mimes', 'dodatech_allow_svg' );

Practical: Customize Email Sender

function dodatech_custom_from_email( $email ) {
    return 'noreply@dodatech.com';
}
add_filter( 'wp_mail_from', 'dodatech_custom_from_email' );

function dodatech_custom_from_name( $name ) {
    return 'DodaTech';
}
add_filter( 'wp_mail_from_name', 'dodatech_custom_from_name' );

Creating Custom Hooks

Your own code can emit hooks so other developers can extend it:

Custom Action Hook

function dodatech_display_author_box( $author_id ) {
    echo '<div class="author-box">';
    echo '<h3>' . get_the_author_meta( 'display_name', $author_id ) . '</h3>';
    echo wpautop( get_the_author_meta( 'description', $author_id ) );

    do_action( 'dodatech_after_author_bio', $author_id );

    echo '</div>';
}

Now any theme or plugin can attach code after the author bio:

function dodatech_add_author_social_links( $author_id ) {
    $twitter = get_the_author_meta( 'twitter', $author_id );
    $linkedin = get_the_author_meta( 'linkedin', $author_id );

    if ( $twitter ) {
        echo '<a href="' . esc_url( $twitter ) . '">Twitter</a>';
    }
    if ( $linkedin ) {
        echo '<a href="' . esc_url( $linkedin ) . '">LinkedIn</a>';
    }
}
add_action( 'dodatech_after_author_bio', 'dodatech_add_author_social_links' );

Custom Filter Hook

function dodatech_get_discount_price( $price, $user_id ) {
    $discount = apply_filters( 'dodatech_discount_percentage', 0, $user_id );

    if ( $discount > 0 && $discount <= 100 ) {
        $price = $price * ( 1 - $discount / 100 );
    }

    return $price;
}

Another developer can modify the discount:

function dodatech_vip_discount( $discount, $user_id ) {
    if ( user_can( $user_id, 'vip' ) ) {
        return 20;
    }
    return $discount;
}
add_filter( 'dodatech_discount_percentage', 'dodatech_vip_discount', 10, 2 );

Checking If Hooks Exist

if ( has_action( 'dodatech_after_author_bio' ) ) {
    // Someone has registered a callback on this action
}

if ( has_filter( 'the_content' ) ) {
    // At least one filter modifies the content
}

// Check if a specific callback is registered
if ( has_action( 'init', 'dodatech_custom_function' ) ) {
    // Our function is attached to init
}

Hook Locations in Template Files

Hooks are not limited to functions.php. You can use them in template files too:

<?php
get_header();
do_action( 'dodatech_before_main_content' );
?>
<main id="primary">
    <?php
    while ( have_posts() ) : the_post();
        get_template_part( 'template-parts/content', get_post_type() );
    endwhile;
    ?>
</main>
<?php
do_action( 'dodatech_after_main_content' );
get_sidebar();
do_action( 'dodatech_before_footer' );
get_footer();

This pattern lets child themes and plugins insert content at specific locations without overriding template files.

Best Practices

  1. Prefix your hook names. Use a unique prefix like your theme or plugin name. dodatech_save_settings not save_settings. This prevents collision.

  2. Document your hooks. Use PHP docblocks describing what the hook does, what parameters it passes, and what the filter should return:

/**
 * Filters the discount percentage for a user.
 *
 * @param int $discount Current discount percentage (0-100)
 * @param int $user_id  ID of the user receiving the discount
 * @return int Modified discount percentage
 */
$discount = apply_filters( 'dodatech_discount_percentage', $discount, $user_id );
  1. Check for function existence. If your plugin depends on another plugin's hooks, check function_exists or class_exists before using them.

  2. Return filter values. Filters must always return the modified data. Forgetting the return statement silently drops the value.

  3. Use priority wisely. Set callbacks that should run early to priority 1-5, default to 10, and late-running ones to 15-20. Avoid priority 0 or very high values unless you know the exact order needed.

Common Mistakes

  1. Forgetting to return a value in a filter callback. Filters receive data, modify it, and must return it. A filter callback without return returns null, which replaces the original data and breaks the output.

  2. Using remove_action without matching priority. remove_action requires the exact same priority used in add_action. If you do not know the priority, pass false as the third parameter to search all priorities (slower but works).

  3. Not checking is_admin() in pre_get_posts. Many beginners filter queries without checking ! is_admin(), causing the filter to run on every AJAX request and admin screen, breaking plugin functionality.

  4. Attaching anonymous functions and trying to remove them. remove_action needs the original function name to unhook it. Anonymous functions (closures) cannot be removed later. Use named functions if you anticipate needing to remove them.

  5. Executing code too early in the execution order. The init hook is the earliest you should register CPTs and taxonomies. The plugins_loaded hook is even earlier for plugin-specific setup. Trying to use WordPress functions before they are loaded causes fatal errors.

Practice Questions

  1. What is the difference between an action and a filter? Give an example of when you would use each.
  2. What happens if you register two functions on the same hook with the same priority? How does WordPress determine execution order?
  3. Why should you never use anonymous functions as hook callbacks if you might need to remove the hook later?

Challenge: Create a plugin that adds a "Reading Time" estimate to the beginning of every post. Use the_content filter to prepend "Estimated reading time: X minutes" to the post content. Calculate reading time by dividing the word count by 200 (average reading speed). Make sure the filter only applies on single post pages, not on archive pages. Add a custom action hook inside the reading time output so other plugins can insert content next to it.

FAQ

### Do hooks affect performance?

Each hook callback adds execution time. Twenty callbacks on the_content mean twenty functions run every time content is displayed. On high-traffic sites, audit hooks for performance. Use profiling tools or the P3 Plugin Profiler to identify slow callbacks.

Can I create hooks in my plugin for other developers?

Yes. Use do_action() for action hooks where developers can run code, and apply_filters() for filter hooks where they can modify data. Document the parameters and expected return values. This is how WooCommerce, ACF, and other major plugins enable extensibility.

What is the difference between do_action and apply_filters?

do_action() executes registered action callbacks and does not expect a return value. apply_filters() executes registered filter callbacks and expects the modified value to be returned. Actions do something; filters modify something.

How do I find all hooks in WordPress core?

Use the Hook Reference on WordPress Codex. For a live view, install the Simply Show Hooks plugin or use Query Monitor. Both display every hook that fires on the current page.

Can I pass multiple arguments to a custom hook?

Yes. Pass additional arguments after the hook name in do_action() and apply_filters(). In the add_action() call, set the accepted_args parameter to match.

Mini Project

Build a plugin that extends WordPress user profiles and customizes emails:

  1. Use show_user_profile and edit_user_profile actions to add custom fields: Twitter URL, LinkedIn URL, and Bio (textarea)
  2. Use profile_update action to save the custom fields
  3. Use wp_mail_from and wp_mail_from_name filters to change all WordPress emails to use your domain
  4. Use the_content filter to append an author box with name, bio, and social links after every single post
  5. Create a custom action hook dodatech_after_author_box so other developers can add content after the author box
  6. Use admin_notices action to show a notice when the plugin is activated

What's Next

Now that you understand WordPress's event system, learn to create reusable content components with shortcodes:

Continue to Lesson 42: Shortcodes — How to create custom shortcodes for content and layout.

Related lessons:

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro