Hooks are the nervous system of WordPress. Without them, extending the CMS would require modifying the source code directly. Actions and filters are what make WordPress an extensible platform rather than just software.
Yet many developers use hooks without truly understanding their mechanics: priority, accepted arguments, conditional hooks, creating custom hooks for your own plugins and themes.
This guide is designed for developers who want to go from "I use hooks" to "I master the hook system." We cover theory, practice, pitfalls, and advanced patterns — with concrete examples from our work at Volade.
Create a free Volade account
Unlock this article's member resources and the full Volade pack.
No credit card · Public checklists stay free.
Actions vs Filters: the fundamental difference
Actions
An action is an execution point in WordPress code where you can add your own code. The action returns nothing — it executes.
// Basic hook
add_action('wp_head', 'my_meta_function');
function my_meta_function() {
echo '<meta name="author" content="Volade">';
}
// Action with parameters
add_action('save_post', 'my_save_log', 10, 3);
function my_save_log($post_id, $post, $update) {
error_log("Post $post_id saved. Update: " . ($update ? 'yes' : 'no'));
}
Filters
A filter receives a value, modifies it, and returns it. It should never cause side effects (echo, redirect, DB insert).
// Basic filter
add_filter('excerpt_length', 'my_excerpt_length');
function my_excerpt_length($length) {
return 30; // 30 words instead of 55
}
// Filter with additional parameters
add_filter('the_content', 'my_add_cta', 20, 1);
function my_add_cta($content) {
if (is_single()) {
$cta = '<p><a href="/contact" class="cta">Contact us</a></p>';
$content .= $cta;
}
return $content;
}
Comparison table
| Criteria | Action | Filter |
|---|---|---|
| Purpose | Execute code | Modify a value |
| Return value | Nothing (void) | The modified value |
| Hook function | do_action() | apply_filters() |
| Hook registration | add_action() | add_filter() |
| Main parameter | Hook name | The value to filter |
| Side effects allowed | Yes | No (ideally) |
Priority and parameters
Understanding priority
Priority (3rd argument) determines the execution order of hooks attached to the same point. Lower numbers execute first.
// Execution order: C (5) → A (10) → B (10) → D (100)
add_action('init', 'function_a', 10);
add_action('init', 'function_b', 10);
add_action('init', 'function_c', 5);
add_action('init', 'function_d', 100);
:::callout-warning
Don't put all your functions at priority 10 by default. If three plugins hook into wp_enqueue_scripts at priority 10, the order is undefined. Use 5, 10, 20, 100 to leave room for others.
:::
Managing parameters
The 4th argument of add_action / add_filter is the number of parameters your function accepts.
// The save_post hook passes 3 parameters
add_action('save_post', 'my_handler', 10, 3);
function my_handler($post_id, $post, $update) {
// $post_id: ID of the saved post
// $post: complete WP_Post object
// $update: boolean (true = update, false = creation)
}
If you don't specify accepted_args, WordPress only passes one parameter — the rest are silently ignored.
| Common mistake | Symptom | Solution |
|---|---|---|
accepted_args not set | Function receives less data than expected | Always specify accepted_args when > 1 |
| Identical priority | Random execution order between plugins | Space priorities (5, 10, 20, 50, 100) |
| Parameter not passed by reference | Changes not persisted | Use &$var if the hook allows it |
Creating your own hooks
When to create a custom hook
- Your plugin or theme needs to be extensible by other developers
- You want to allow modifying values without touching your code
- You have strategic points in your code where others might need to intervene
Creating a custom action
// In your plugin
function volade_send_welcome_email($user_id) {
$user = get_userdata($user_id);
// Send email...
// Allow other plugins to hook after sending
do_action('volade_after_welcome_email', $user_id, $user);
}
// In another plugin or theme
add_action('volade_after_welcome_email', 'my_add_email_log', 10, 2);
function my_add_email_log($user_id, $user) {
error_log("Welcome email sent to {$user->user_email}");
}
Creating a custom filter
// In your plugin
function volade_product_price($product_id) {
$price = get_post_meta($product_id, 'price', true);
// Allow extensions to modify the price
return apply_filters('volade_product_price', $price, $product_id);
}
// In an extension
add_filter('volade_product_price', 'my_apply_discount', 10, 2);
function my_apply_discount($price, $product_id) {
if (has_term('sale', 'product_tag', $product_id)) {
return $price * 0.9; // -10%
}
return $price;
}
Naming conventions
| Convention | Example | Usage |
|---|---|---|
{plugin}_{action}_ | volade_before_email_send | Actions |
{plugin}_filter_{name} | volade_filter_price | Filters (alternative) |
{plugin}_{resource}_{action} | volade_order_before_payment | Specific actions |
{plugin}_{resource}_filter | volade_order_filter_total | Specific filters |
Conditional hooks
Run a hook only in certain contexts
// Conditional hook based on capability
if (current_user_can('manage_options')) {
add_action('admin_bar_menu', 'my_add_admin_bar_link', 100);
}
// Conditional hook based on post type
if (get_post_type() === 'product') {
add_filter('the_content', 'my_product_cta');
}
// Conditional hook based on a specific page
add_action('template_redirect', function() {
if (is_page('contact')) {
add_action('wp_footer', 'my_contact_tracking_script');
}
});
Native WordPress conditional hooks
| Hook | Trigger |
|---|---|
init | WordPress loaded, before any headers |
wp_loaded | All WordPress requests handled |
template_redirect | Before template selection |
wp_enqueue_scripts | Loading scripts and styles |
admin_enqueue_scripts | Loading admin scripts |
save_post | Saving a post |
wp_login | User login |
wp_logout | User logout |
user_register | New user created |
deleted_post | Post deleted |
Real-world hook examples
Example 1: Add a custom admin column
// Add the column
add_filter('manage_posts_columns', 'add_excerpt_column');
function add_excerpt_column($columns) {
$columns['excerpt'] = 'Excerpt';
return $columns;
}
// Display column content
add_action('manage_posts_custom_column', 'display_excerpt_column', 10, 2);
function display_excerpt_column($column_name, $post_id) {
if ($column_name === 'excerpt') {
echo wp_trim_words(get_the_excerpt($post_id), 20);
}
}
Example 2: Modify the main query
add_action('pre_get_posts', 'modify_archive_query');
function modify_archive_query($query) {
if (!is_admin() && $query->is_main_query() && is_post_type_archive('product')) {
$query->set('posts_per_page', 24);
$query->set('orderby', 'meta_value_num');
$query->set('meta_key', 'price');
$query->set('order', 'ASC');
}
}
Example 3: Custom login redirect
add_filter('login_redirect', 'redirect_after_login', 10, 3);
function redirect_after_login($redirect_to, $request, $user) {
if (isset($user->roles) && in_array('subscriber', $user->roles)) {
return home_url('/my-account/');
}
return $redirect_to;
}
Best practices
Well-written hook checklist
- Unique hook name prefixed with your namespace
- Explicit priority spaced from others
accepted_argsalways specified when > 1- Documentation of the hook (parameters, return type, example)
- Hook fires at the right moment in the WordPress lifecycle
- In a filter: always return the modified value (never null unless explicit)
- In an action: no return value used by the caller
Anti-patterns to avoid
// AVOID: filter with side effects
add_filter('the_title', 'bad_filter');
function bad_filter($title) {
update_post_meta(get_the_ID(), 'last_access', time()); // SIDE EFFECT
return $title;
}
// AVOID: overly broad priority
add_action('init', 'generic_function', 10); // 10,000 plugins on init at 10
add_action('init', 'important_function', 10); // random order
// AVOID: missing parameters
add_action('save_post', 'my_handler'); // receives 1 parameter, not 3
What we take away
WordPress's hook system is both simple and deep. Actions and filters share the same mechanics but serve distinct purposes.
Master priorities: don't leave execution order to chance. Space your priorities (5, 10, 20, 50, 100) to leave room for other extensions.
Always specify accepted_args: it's the #1 source of silent bugs in hooks. If you expect 3 parameters, write add_action('hook', 'func', 10, 3).
Create custom hooks: this is what separates a closed plugin from an extensible one. do_action('my_prefix_event') and apply_filters('my_prefix_filter', $value) are your best tools.
Document your hooks: a hook without documentation is a hook nobody will use.
Article updated July 9, 2026. Sources: WordPress Plugin Developer Handbook, Code Reference documentation, Volade experience feedback.
Ready to take action?
Explore the Volade catalog — no account required to get started.
Your feedback matters
Comment on “WordPress Hooks 2026: complete developer guide — actions, filters, creation, best practices” or rate this article to help the community.
people shared this article