Accepted Arguments
$accepted_args tells WordPress how many of a hook's arguments to pass your callback; get it wrong and PHP 8 throws ArgumentCountError.
add_action( string $hook_name, callable $callback, int $priority = 10, int $accepted_args = 1 )The fourth parameter of add_action() and add_filter() is $accepted_args, and it defaults to 1. It is not a description of your function — it is an instruction. WP_Hook uses it to decide how many of the hook's arguments to hand over.
The actual mechanism
In WordPress 6.8, WP_Hook::apply_filters() does three things. If $accepted_args is 0, your callback is called with no arguments at all. If it is greater than or equal to the number of arguments available, all of them are passed. Otherwise the argument list is cut with array_slice() to that length.
Nothing is padded with null
Asking for more arguments than the hook supplies does not produce nulls. WordPress passes only what exists, so a required parameter with nothing to fill it is an ArgumentCountError on PHP 8. The default of 1 causes the mirror-image failure far more often: a callback declaring three required parameters, attached to save_post without $accepted_args set to 3, fatals the moment anyone saves a post.
Match the source
Look up the do_action() or apply_filters() call in the Code Reference and count. One quirk worth knowing: an action fired with no arguments at all, such as init, actually passes a single empty string, so a callback declaring one parameter receives '' rather than erroring.
Example
<?php
// Core fires: do_action( 'save_post', $post_id, $post, $update ) — three arguments.
// BROKEN: $accepted_args defaults to 1, so WordPress calls acme_touch( $post_id )
// and PHP throws ArgumentCountError for $post and $update.
add_action( 'save_post', 'acme_touch' );
function acme_touch( $post_id, $post, $update ) {}
// CORRECT: ask for all three.
add_action( 'save_post', 'acme_touch', 10, 3 );
function acme_touch( $post_id, $post, $update ) {
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( $update ) {
update_post_meta( $post_id, 'acme_last_editor', get_current_user_id() );
}
}
// ALSO CORRECT: take only the arguments you need. Leaving $accepted_args at 1
// means WordPress slices the list down to one, and the rest are never passed.
add_action( 'save_post', 'acme_clear_cache' );
function acme_clear_cache( $post_id ) {
delete_transient( 'acme_related_' . $post_id );
}When to use it
- Receive the WP_Post object as well as the ID inside a save_post callback.
- Take the second context argument on the_title so you know which post is being filtered.
- Attach a callback that ignores the hook's arguments entirely, using accepted_args of 0.
- Diagnose an ArgumentCountError that only appears when a specific action fires.
More examples
accepted_args of 0, and the no-argument action quirk
Use accepted_args of 0 when you attach an existing function that takes no parameters to a hook that supplies some — it stops PHP complaining about surplus arguments in strict setups and documents the intent. The init quirk explains why a stray parameter there never errors, which is a common source of confusion when learning the rules.
// Core itself uses 0 when a callback must take nothing. From default-filters.php:
// add_action( 'wp_head', 'rest_output_link_wp_head', 10, 0 );
// rest_output_link_wp_head() declares no parameters, and 0 guarantees none are passed.
add_action( 'wp_head', 'acme_meta_tag', 10, 0 );
function acme_meta_tag() {
echo '<meta name="acme-version" content="1.4.0">' . "\n";
}
// The quirk: do_action( 'init' ) passes no arguments, but plugin.php converts an
// empty argument list into array( '' ). So a one-parameter callback on init gets ''.
add_action( 'init', 'acme_init' );
function acme_init( $nothing ) {
// $nothing === '' — no error, but the parameter is meaningless. Do not declare it.
}Reading the source to get the count right
The habit worth forming: count the arguments at the do_action/apply_filters call, then set accepted_args to match what you actually declare. Giving trailing parameters defaults protects generic callbacks that get attached to several hooks over time.
/*
* Look up the firing call before you write the callback. In WordPress 6.8:
*
* the_content apply_filters( 'the_content', $content ) 1 arg
* the_title apply_filters( 'the_title', $post_title, $post_id ) 2 args
* save_post do_action( 'save_post', $post_id, $post, $update ) 3 args
* pre_get_posts do_action_ref_array( 'pre_get_posts', array( &$this ) ) 1 arg
*
* do_action_ref_array( $hook, $args ) is the same as do_action( $hook, ...$args );
* it only differs in taking the arguments as an array.
*/
add_filter( 'the_title', 'acme_title', 10, 2 );
function acme_title( $title, $post_id = 0 ) {
// Defaulting trailing parameters is cheap insurance: if this callback is ever
// reused on a hook that passes fewer arguments, it degrades instead of fataling.
return $post_id ? $title : $title;
}
Discussion