Prior to WordPress 7.1, developers interacting with the Abilities API could monitor execution through the wp_before_execute_ability and wp_after_execute_ability action hooks. While these actions were sufficient for observing events or logging execution metrics, they provided no mechanism to modify input parameters, override authorization decisions, transform execution results, or alter the execution pipeline. Introduced in Trac ticket #64989 and changeset #62397, WordPress 7.1 introduces four execution lifecycle filters that grant plugins complete programmatic control over how abilities execute.
Understanding the Abilities API Execution Lifecycle in WordPress 7.1
The core execution method WP_Ability::execute() runs through a structured pipeline that processes input, validates permissions, invokes the registered callback, and validates output. The newly added lifecycle filters are strategically placed before and after these processing steps.
The updated lifecycle proceeds in the following sequential order:
wp_pre_execute_ability(Allows immediate short-circuiting of the entire pipeline)WP_Ability::normalize_input()wp_ability_normalize_inputWP_Ability::validate_input()WP_Ability::check_permissions()wp_ability_permission_resultwp_before_execute_ability(Existing action hook)- Registered execution callback
wp_ability_execute_resultWP_Ability::validate_output()wp_after_execute_ability(Existing action hook)- Return result to caller
Input and output transformations occur directly before their respective schema-validation phases. Transformed data must continue to conform to the ability’s registered JSON schemas. The only filter that bypasses schema validation entirely is wp_pre_execute_ability, which short-circuits the pipeline before any normalization, validation, or permission logic occurs.
Short-Circuiting Execution with wp_pre_execute_ability
The wp_pre_execute_ability filter fires at the very start of WP_Ability::execute(). It accepts four arguments: the precomputed return value $pre, the string $ability_name, the raw $input, and the WP_Ability instance.
apply_filters( 'wp_pre_execute_ability', $pre, $ability_name, $input, $ability );
If a callback returns $pre unchanged, normal execution continues. Returning any other value immediately short-circuits the pipeline, bypassing input normalization, schema validation, permission checks, and the execution callback.
Common use cases for this hook include caching execution results, rate limiting, implementing approval workflows, placing abilities into maintenance mode, or mocking ability returns during testing.
For example, to temporarily disable an ability during site maintenance and return an error without running validation or permission logic:
add_filter( 'wp_pre_execute_ability', function ( $pre, $ability_name, $input, $ability ) {
if ( 'my-plugin/sync-catalog' !== $ability_name ) {
return $pre;
}
if ( ! get_option( 'my_plugin_maintenance_mode', false ) ) {
return $pre;
}
return new WP_Error(
'ability_temporarily_unavailable',
__( 'This operation is temporarily unavailable due to maintenance.', 'my-plugin' ),
array( 'status' => 503 )
);
}, 10, 4 );
Because wp_pre_execute_ability runs before authorization checks or input validation, developers should restrict its logic to narrow decisions that do not depend on validated parameters or current user capabilities.
Transforming Input with wp_ability_normalize_input
The wp_ability_normalize_input filter runs inside WP_Ability::normalize_input() right after default values defined in the ability’s input schema have been applied.
apply_filters( 'wp_ability_normalize_input', $input, $ability_name, $ability );
This filter allows developers to inject dynamic defaults that cannot be declared statically in JSON Schema, normalize inconsistent incoming values, enrich AI context prompts, or attach caller metadata.
The following example injects contextual metadata into the normalized input structure:
add_filter( 'wp_ability_normalize_input', function ( $input, $ability_name, $ability ) {
if ( 'my-plugin/process-content' !== $ability_name ) {
return $input;
}
if ( ! is_array( $input ) ) {
$input = array();
}
$input['requesting_user_id'] = get_current_user_id();
$input['site_url'] = home_url();
return $input;
}, 10, 3 );
If a callback attached to wp_ability_normalize_input returns a WP_Error, execution halts before validation, permission checks, or the callback run. When executing via the Abilities REST API, a returned WP_Error propagates directly to the REST controller, defaulting to an HTTP status code of 400 unless an explicit status (such as 422 or 429) is specified in the error parameters:
add_filter( 'wp_ability_normalize_input', function ( $input, $ability_name ) {
if ( 'my-plugin/process-content' !== $ability_name ) {
return $input;
}
if ( my_plugin_rate_limit_exceeded() ) {
return new WP_Error(
'ability_rate_limit_exceeded',
__( 'The ability rate limit has been exceeded.', 'my-plugin' ),
array( 'status' => 429 )
);
}
return $input;
}, 10, 2 );
Filtering Permission Results with wp_ability_permission_result
The wp_ability_permission_result filter fires within WP_Ability::check_permissions() after the ability’s primary permission_callback has evaluated.
apply_filters( 'wp_ability_permission_result', $permission, $ability_name, $input, $ability );
The filter accepts and returns:
trueto grant permission.falseto deny execution.- A
WP_Errorobject to reject execution with a specific error code, message, and status.
Any non-boolean, non-WP_Error return value is cast to false. Because this filter operates inside check_permissions(), it automatically applies across all invocation entry points, including direct code calls, REST API endpoints, and WP-CLI commands.
The following example applies a secondary administrator access policy to a record deletion ability:
add_filter( 'wp_ability_permission_result', function ( $permission, $ability_name, $input, $ability ) {
if ( 'my-plugin/delete-records' !== $ability_name ) {
return $permission;
}
// Preserve existing denials and WP_Error objects.
if ( false === $permission || is_wp_error( $permission ) ) {
return $permission;
}
if ( ! current_user_can( 'manage_options' ) ) {
return new WP_Error(
'ability_additional_permission_required',
__( 'This operation requires administrator access.', 'my-plugin' )
);
}
return true;
}, 10, 4 );
Developers must exercise extreme caution when modifying permission results: returning true from this filter overrides an explicit denial returned by the original permission_callback.
Transforming and Recovering Results with wp_ability_execute_result
The wp_ability_execute_result filter fires after the ability’s execution callback has completed and before output schema validation is performed.
apply_filters( 'wp_ability_execute_result', $result, $ability_name, $input, $ability );
This hook allows plugins to format responses, strip sensitive internal metadata, execute content safety checks, convert successful returns into errors, or recover gracefully from execution failures.
To sanitize output by removing internal debugging keys before output validation:
add_filter( 'wp_ability_execute_result', function ( $result, $ability_name, $input, $ability ) {
if ( 'my-plugin/get-report' !== $ability_name || is_wp_error( $result ) || ! is_array( $result ) ) {
return $result;
}
unset( $result['internal_debug_data'] );
return $result;
}, 10, 4 );
The filter also receives any WP_Error objects generated during execution, enabling fallbacks for transient upstream errors:
add_filter( 'wp_ability_execute_result', function ( $result, $ability_name, $input, $ability ) {
if ( 'my-plugin/get-remote-data' !== $ability_name || ! is_wp_error( $result ) || 'remote_service_unavailable' !== $result->get_error_code() ) {
return $result;
}
$fallback = my_plugin_get_fallback_data();
/*
* The fallback must conform to the ability's registered
* output_schema because it will be validated after this filter.
*/
return $fallback;
}, 10, 4 );
The WP_Filter_Sentinel Marker Class
To reliably distinguish between an un-modified filter parameter and intentional user return values, WordPress 7.1 introduces the WP_Filter_Sentinel class. Loaded alongside WP_Hook, Core uses a unique instance of this class as the default value for wp_pre_execute_ability.
By using object identity comparison (===), Core can determine whether a callback returned the initial default value or explicitly returned valid values like null, false, 0, or empty arrays. Callbacks do not need to instantiate WP_Filter_Sentinel directly; returning the original $pre argument tells Core to continue standard execution.
Summary of New Filters
| Filter Hook | Execution Location | Primary Purpose |
|---|---|---|
wp_pre_execute_ability |
Start of WP_Ability::execute() |
Short-circuit execution, return early cached or error responses. |
wp_ability_normalize_input |
Inside WP_Ability::normalize_input() |
Transform normalized input or add dynamic request metadata before validation. |
wp_ability_permission_result |
Inside WP_Ability::check_permissions() |
Override, supplement, or refine authorization check results. |
wp_ability_execute_result |
After execute callback execution | Transform return data or recover from execution failures before schema validation. |
Backward Compatibility and Schema Safeguards
These lifecycle additions are fully additive. Existing abilities, permission callbacks, and execution callbacks operate unchanged if these filters are not implemented. Existing observer actions (wp_before_execute_ability and wp_after_execute_ability) continue to trigger normally.
Except for complete short-circuiting via wp_pre_execute_ability, schema validation remains an absolute boundary. Any inputs modified by wp_ability_normalize_input must satisfy the ability’s input_schema, and any outputs modified or recovered via wp_ability_execute_result must satisfy the registered output_schema.
Frequently asked questions
What are the four new execution lifecycle filters in WordPress 7.1?
WordPress 7.1 introduces wp_pre_execute_ability, wp_ability_normalize_input, wp_ability_permission_result, and wp_ability_execute_result.
How does wp_pre_execute_ability determine if execution should continue?
Core uses a WP_Filter_Sentinel instance as the initial value for $pre. If a filter callback returns $pre unchanged, execution continues normally. Returning any other value immediately short-circuits the pipeline.
Can wp_ability_permission_result override an permission denial?
Yes. Returning true from wp_ability_permission_result can override a denial from the primary permission_callback, so developers should exercise extreme caution when modifying permission checks.
Do transformed results still undergo JSON schema validation?
Yes. Transformed inputs are checked by validate_input() and transformed results are checked by validate_output(). Only wp_pre_execute_ability bypasses schema validation.
Primary reference: Review the original announcement for exact release details. This article is an independent explanation and does not reproduce the source text.