Abilities API gains custom validation filters and invocation lifecycle hook
v7.1
WordPress 7.1 expands the Abilities API with custom validation hooks, a new invocation lifecycle action, richer user information, selective field responses, and more consistent schemas for the core abilities introduced in WordPress 6.9.
- Custom input and output validation
- Observing every ability invocation
- Expanded core/get-user-info responses
- Consistent schemas across core abilities
- Typed input for REST ability runs
Custom input and output validation
WP_Ability validates input and output against each ability’s JSON Schema. WordPress 7.1 adds two filters that let extenders supplement that validation:
wp_ability_validate_inputwp_ability_validate_output
Each filter receives the existing validation result, the value being validated, and the ability name:
/**
* @param true|WP_Error $is_valid Validation result.
* @param mixed $value Input or output value.
* @param string $name Ability name.
*/
For example, a plugin can enforce a rule that cannot be expressed by the JSON Schema implementation used by WordPress:
add_filter(
'wp_ability_validate_input',
function ( $is_valid, $input, $ability_name ) {
if ( 'my-plugin/send-message' !== $ability_name ) {
return $is_valid;
}
// Preserve errors produced by the default schema validation.
if ( is_wp_error( $is_valid ) ) {
return $is_valid;
}
if (
! is_array( $input )
|| empty( $input['recipient'] )
|| ! str_ends_with( $input['recipient'], '@example.com' )
) {
return new WP_Error(
'invalid_recipient',
__( 'The recipient must use the example.com domain.', 'my-plugin' )
);
}
return true;
},
10,
3
);
Output can be validated in the same way:
add_filter(
'wp_ability_validate_output',
function ( $is_valid, $output, $ability_name ) {
if ( 'my-plugin/send-message' !== $ability_name ) {
return $is_valid;
}
if ( is_wp_error( $is_valid ) ) {
return $is_valid;
}
if ( ! is_array( $output ) || empty( $output['message_id'] ) ) {
return new WP_Error(
'invalid_message_output',
__( 'The ability did not return a message ID.', 'my-plugin' )
);
}
return true;
},
10,
3
);
Callbacks should return true when the value is valid or a WP_Error describing why it is invalid. Returning false also fails validation, but WordPress converts it to a generic WP_Error. The filters operate on the validation result rather than the schema, allowing plugins to augment an existing error or reject data that otherwise passed JSON Schema validation.
REST-style validate_callback and sanitize_callback schema keywords are not executed by the Abilities API. Custom runtime validation should use these new filters. See ticket #64311.
Observing every ability invocation
WordPress 7.1 adds the wp_ability_invoked action at the beginning of WP_Ability::execute():
do_action( 'wp_ability_invoked', $this->name, $input, $this );
The action fires before input normalization, validation, permission checks, and the wp_pre_execute_ability short-circuit filter. Consequently, it runs for every invocation, including calls that:
- contain invalid input;
- fail their permission check;
- are short-circuited;
- return a cached result;
- require approval; or
- proceed to the execution callback.
This makes the action suitable for auditing, telemetry, tracing, and invocation accounting:
add_action(
'wp_ability_invoked',
function ( $ability_name, $input, $ability ) {
// Avoid storing sensitive input without appropriate filtering.
do_action(
'my_plugin_record_ability_invocation',
array(
'ability' => $ability_name,
'timestamp' => time(),
)
);
},
10,
3
);
The action receives raw, unnormalized input. Plugins should therefore avoid logging input indiscriminately, because it may contain credentials, personal information, or other sensitive data.
The existing wp_before_execute_ability and wp_after_execute_ability actions also receive the corresponding WP_Ability instance as an additional final argument. Existing callbacks continue to work unchanged. To receive the new argument, update the callback signature and $accepted_args. See ticket #65248.
Expanded core/get-user-info responses
The core/get-user-info ability now returns five additional profile fields for the current authenticated user:
first_namelast_namenicknamedescriptionuser_url
When no input is supplied, the response contains all supported properties:
{
"id": 1,
"display_name": "Jane Doe",
"user_nicename": "jane-doe",
"user_login": "jane",
"roles": [ "administrator" ],
"locale": "en_US",
"first_name": "Jane",
"last_name": "Doe",
"nickname": "Jane",
"description": "Site administrator and contributor.",
"user_url": "https://example.com"
}
The roles property is now normalized with array_values() so that it is consistently encoded as a JSON array, regardless of its PHP array keys.
Callers can also use the new optional fields input property to request a subset of the response:
$ability = wp_get_ability( 'core/get-user-info' );
$result = $ability->execute(
array(
'fields' => array(
'display_name',
'first_name',
'last_name',
),
)
);
The resulting value contains only the requested properties:
{
"display_name": "Jane Doe",
"first_name": "Jane",
"last_name": "Doe"
}
Supported field names are declared through an enum in the input schema. Passing an unknown name causes execution to return an ability_invalid_input error before the ability callback runs.
The permission requirement remains unchanged: the requester must be logged in. See ticket #65234 and changeset [62419].
Consistent schemas across core abilities
The following core abilities now follow the same schema conventions:
core/get-site-infocore/get-user-infocore/get-environment-info
Every output property declares a translatable, Title Case title and a description. This provides better metadata to REST, MCP, WebMCP, AI, and other programmatic clients that use the schema to present or select ability fields.
core/get-environment-info now supports the same optional fields input as the other two abilities:
$ability = wp_get_ability( 'core/get-environment-info' );
$result = $ability->execute(
array(
'fields' => array( 'php_version' ),
)
);
Unknown field names are rejected through schema validation.
In addition, core/get-user-info is now exposed through the REST API. Like the other core abilities, it declares the new public meta flag introduced in WordPress 7.1. Authenticated clients can discover it through:
/wp-json/wp-abilities/v1/abilities
The exact ordered set of input field names and output properties is covered by registration tests. Developers extending or consuming these core abilities should use the schemas for discovery rather than assuming a fixed response shape. See ticket #65355.
Typed input for REST ability runs
REST requests that run an ability over GET or DELETE deliver every query string value as a string, and a comma-separated list as a single string. In previous releases, the ability received this input as-is: an integer arrived as "10", a boolean as "true", and strict comparisons inside the callback silently failed unless the ability hand-rolled its own casting.
WordPress 7.1 coerces the input of a run request to the types declared in the ability’s input_schema before the ability runs. The coercion is registered as the input argument’s sanitize_callback, so the permission callback and the execute callback both receive natively typed input from the same request object:
GET /wp-json/wp-abilities/v1/abilities/my-plugin/list-items/run
?input[limit]=10&input[featured]=true&input[ids]=1,2,3
With an input schema declaring limit as an integer, featured as a boolean, and ids as an array of integers, the callbacks now receive:
array(
'limit' => 10,
'featured' => true,
'ids' => array( 1, 2, 3 ),
)
Coercion never changes what validation accepts. Input is coerced only when validate_input() already accepts it, so invalid input reaches validation untouched and returns the same ability_invalid_input error as before.
Props to @benjamin_zekavica and @annezazu for peer review, and to @jorgefilipecosta for review and collaboration.
Fetched July 31, 2026


