WordPress Hooks — Actions and Filters Complete Developer Guide
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
Prefix your hook names. Use a unique prefix like your theme or plugin name.
dodatech_save_settingsnotsave_settings. This prevents collision.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 );
Check for function existence. If your plugin depends on another plugin's hooks, check
function_existsorclass_existsbefore using them.Return filter values. Filters must always return the modified data. Forgetting the
returnstatement silently drops the value.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
Forgetting to return a value in a filter callback. Filters receive data, modify it, and must return it. A filter callback without
returnreturnsnull, which replaces the original data and breaks the output.Using remove_action without matching priority.
remove_actionrequires the exact same priority used inadd_action. If you do not know the priority, passfalseas the third parameter to search all priorities (slower but works).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.Attaching anonymous functions and trying to remove them.
remove_actionneeds the original function name to unhook it. Anonymous functions (closures) cannot be removed later. Use named functions if you anticipate needing to remove them.Executing code too early in the execution order. The
inithook is the earliest you should register CPTs and taxonomies. Theplugins_loadedhook is even earlier for plugin-specific setup. Trying to use WordPress functions before they are loaded causes fatal errors.
Practice Questions
- What is the difference between an action and a filter? Give an example of when you would use each.
- What happens if you register two functions on the same hook with the same priority? How does WordPress determine execution order?
- 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
Mini Project
Build a plugin that extends WordPress user profiles and customizes emails:
- Use
show_user_profileandedit_user_profileactions to add custom fields: Twitter URL, LinkedIn URL, and Bio (textarea) - Use
profile_updateaction to save the custom fields - Use
wp_mail_fromandwp_mail_from_namefilters to change all WordPress emails to use your domain - Use
the_contentfilter to append an author box with name, bio, and social links after every single post - Create a custom action hook
dodatech_after_author_boxso other developers can add content after the author box - Use
admin_noticesaction 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:
- WP_Query and The Loop — Use hooks to modify queries before they execute
- PHP Programming Guide — Deepen your PHP knowledge for writing robust hooks
- Custom Post Types — Hook save_post to Process CPT data automatically
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro