The Abilities API gives WordPress plugins a common language for describing and executing units of functionality. An ability has a stable name, human-readable metadata, input and output contracts, a permission check, and an execution callback. Other PHP code can discover it without knowing which plugin registered it, while REST, WP-CLI, JavaScript, MCP, and AI integrations can use the same description to understand how to call it.
WordPress 7.1, scheduled for release on August 19, 2026, fills in several pieces that matter once abilities move beyond simple demos. Actions can observe every invocation and every validated success. Filters can normalize input, extend validation, enforce policy, short-circuit execution, or transform results. Client-facing JSON Schemas now go through a shared portable representation. And both wp_get_abilities() and the REST collection endpoint can select abilities by category, namespace, and nested metadata.
This post walks through the execution lifecycle, the schema preparation layer, and the discovery filters, with the trade-offs I’d weigh before reaching for each one.
What a well-described ability looks like
The example below registers a category and a read-only ability that summarizes a post. Categories must be registered before abilities, using their respective initialization actions.
add_action( 'wp_abilities_api_categories_init', static function (): void { wp_register_ability_category( 'my-plugin-content', array( 'label' => __( 'My Plugin Content', 'my-plugin' ), 'description' => __( 'Abilities that inspect and summarize content.', 'my-plugin' ), ) ); });add_action( 'wp_abilities_api_init', static function (): void { wp_register_ability( 'my-plugin/summarize-post', array( 'label' => __( 'Summarize a post', 'my-plugin' ), 'description' => __( 'Returns a plain-text summary of a post.', 'my-plugin' ), 'category' => 'my-plugin-content', 'input_schema' => array( 'type' => 'object', 'properties' => array( 'post_id' => array( 'type' => 'integer', 'minimum' => 1, 'description' => __( 'Post to summarize.', 'my-plugin' ), ), 'max_words' => array( 'type' => 'integer', 'minimum' => 10, 'maximum' => 200, 'default' => 50, 'description' => __( 'Upper bound on the summary length.', 'my-plugin' ), ), 'locale' => array( 'type' => 'string', 'description' => __( 'Locale for the summary, defaulting to the site locale.', 'my-plugin' ), ), ), 'required' => array( 'post_id' ), 'additionalProperties' => false, ), 'output_schema' => array( 'type' => 'object', 'properties' => array( 'post_id' => array( 'type' => 'integer', 'description' => __( 'The post that was summarized.', 'my-plugin' ), ), 'summary' => array( 'type' => 'string', 'minLength' => 1, 'description' => __( 'The summary, as plain text.', 'my-plugin' ), ), ), 'required' => array( 'post_id', 'summary' ), 'additionalProperties' => false, ), 'permission_callback' => static function ( array $input ) { if ( ! current_user_can( 'read_post', $input['post_id'] ) ) { return new WP_Error( 'my_plugin_cannot_read_post', __( 'You are not allowed to read this post.', 'my-plugin' ) ); } return true; }, 'execute_callback' => static function ( array $input ) { $post = get_post( $input['post_id'] ); if ( ! $post instanceof WP_Post ) { return new WP_Error( 'my_plugin_post_not_found', __( 'The requested post could not be found.', 'my-plugin' ), array( 'status' => 404 ) ); } $content = wp_strip_all_tags( strip_shortcodes( $post->post_content ) ); $max_words = $input['max_words'] ?? 50; $locale = $input['locale'] ?? get_locale(); // Returns a WP_Error with the code `summary_service_unavailable` on failure. $summary = my_plugin_generate_summary( $content, $max_words, $locale ); if ( is_wp_error( $summary ) ) { return $summary; } return array( 'post_id' => (int) $post->ID, 'summary' => $summary, ); }, 'meta' => array( 'public' => true, 'annotations' => array( 'readonly' => true, 'destructive' => false, 'idempotent' => true, ), ), ) ); });
The public metadata flag added in WordPress 7.1 declares that an ability is intended for clients. It provides the default for channel-specific exposure flags. For REST, an explicit show_in_rest value takes precedence over public, so a plugin can declare broad intent while disabling one channel:
'meta' => array( 'public' => true, 'show_in_rest' => false,),
Exposure isn’t authorization. A discoverable ability still runs its permission_callback on every execution.
Finding the abilities you need
Before WordPress 7.1, consumers commonly called wp_get_abilities() and built their own array_filter() pass over the result. The function now provides one shared filtering pipeline:
$abilities = wp_get_abilities( array( 'category' => 'my-plugin-content', 'namespace' => 'my-plugin', 'meta' => array( 'public' => true, 'annotations' => array( 'readonly' => true, ), ), ));
| Argument | Matching behavior |
|---|---|
category | Exact match against one category slug. |
namespace | Matches the namespace prefix of an ability name. Pass it without the trailing slash. |
meta | Recursively matches nested metadata using strict comparison. Every supplied condition must match. |
Different argument types use AND logic. In the example, an ability must be in the category, use the namespace, be public, and be read-only. Strict metadata comparison means true, 1, and 'true' aren’t interchangeable in PHP, a distinction that matters as soon as metadata arrives from a query string rather than from PHP source. The filtered array is keyed by ability name until a result callback or a global result filter deliberately reshapes it.
Caller-scoped callbacks
Two callbacks handle conditions and shaping that don’t belong in the shared declarative API:
$abilities = wp_get_abilities( array( 'namespace' => 'my-plugin', 'item_include_callback' => static function ( WP_Ability $ability ): bool { $meta = $ability->get_meta(); return $meta['my_plugin']['enabled'] ?? true; }, 'result_callback' => static function ( array $abilities ): array { uasort( $abilities, static function ( WP_Ability $a, WP_Ability $b ): int { return strcasecmp( $a->get_label(), $b->get_label() ); } ); return array_slice( $abilities, 0, 10, true ); }, ));
item_include_callback receives each ability that survived the declarative checks. result_callback receives the complete matched array and may sort, paginate, or reshape it. These callbacks are local to one query and don’t affect other consumers, which makes them the right tool for anything specific to a single integration.
Ecosystem-wide filters
The ecosystem-scoped equivalents are wp_get_abilities_item_include and wp_get_abilities_result:
add_filter( 'wp_get_abilities_item_include', static function ( bool $include, WP_Ability $ability, array $args ): bool { if ( ! $include ) { return false; } $meta = $ability->get_meta(); return $meta['my_plugin']['enabled'] ?? true; }, 10, 3);
Declarative matching on category, namespace, and meta runs first, then the caller’s item_include_callback, then wp_get_abilities_item_include, then the caller’s result_callback, and finally wp_get_abilities_result for the complete array.
The caller’s verdict is a starting value, not a veto. An ability the caller’s callback just excluded still reaches the global filter, which can return true and put it back in the result. That’s why the example above upholds a prior denial explicitly instead of assuming the pipeline will.
Both global filters run even when the caller passes no arguments. That makes them suitable for system-wide discovery policy, but it also means wp_get_abilities() is no longer a guaranteed raw-registry read once another plugin attaches a filter. Code that specifically requires the unfiltered registry contents can use WP_Abilities_Registry::get_all_registered(). Use that bypass deliberately, because most integrations benefit from participating in the common pipeline so that ecosystem policy is respected.
Discovery over REST
The REST collection endpoint delegates to wp_get_abilities() and exposes the declarative filters as query parameters:
GET /wp-json/wp-abilities/v1/abilities?namespace=my-pluginGET /wp-json/wp-abilities/v1/abilities?category=my-plugin-contentGET /wp-json/wp-abilities/v1/abilities?meta[annotations][readonly]=true
Parameters can be combined and use the same AND logic. The known readonly, destructive, and idempotent annotation values are coerced from query strings to booleans before strict matching.
Every collection request also forces meta.show_in_rest = true internally. Supplying another metadata query can’t reveal an ability that is hidden from REST. The endpoint still requires an authenticated WordPress user, and executing a listed ability still requires its permission callback to pass.
Custom metadata needs a REST parameter schema if its query-string values should be coerced before strict comparison. Without one, 'true' will never match boolean true. The rest_abilities_collection_params filter extends the collection argument schema:
add_filter( 'rest_abilities_collection_params', static function ( array $params ): array { $params['meta']['properties']['my_plugin'] = array( 'type' => 'object', 'properties' => array( 'enabled' => array( 'type' => 'boolean', ), ), ); return $params; });
After that declaration, a request such as the following compares boolean true with boolean true:
GET /wp-json/wp-abilities/v1/abilities?meta[my_plugin][enabled]=true
Registration filters
Two filters run before registration arguments are validated and their objects are created:
add_filter( 'wp_register_ability_args', $callback, 10, 2 );add_filter( 'wp_register_ability_category_args', $callback, 10, 2 );
wp_register_ability_args receives the arguments and the namespaced ability name. wp_register_ability_category_args receives the arguments and the category slug. They are useful when an integration needs to seed the metadata it later queries:
add_filter( 'wp_register_ability_args', static function ( array $args, string $ability_name ): array { if ( ! str_starts_with( $ability_name, 'my-plugin/' ) ) { return $args; } $disabled = (array) get_option( 'my_plugin_disabled_abilities', array() ); $args['meta']['my_plugin']['enabled'] = ! in_array( $ability_name, $disabled, true ); return $args; }, 10, 2);
This is the flag the discovery examples above read.
Because these filters run before validation, a malformed change can prevent registration entirely. Namespace the conditions, preserve unrelated arguments, and make the smallest change that works.
Actions observe, filters decide
The usual WordPress distinction is especially useful here. Use an action to observe an event, such as recording that an invocation occurred or measuring a successful execution. Use a filter when a callback must return the value that the execution pipeline will then use. Filters can change control flow, input, permissions, validation, and output.
WordPress 7.1 makes the complete sequence look like this:
WP_Ability::execute()│├─ wp_ability_invoked action every attempt, before any checks├─ wp_pre_execute_ability filter any return value skips the rest│├─ ::normalize_input()│ ├─ top-level default built-in only when the whole input is null│ └─ wp_ability_normalize_input filter reshape or reject the input│├─ ::validate_input()│ ├─ schema validation built-in checked against input_schema│ └─ wp_ability_validate_input filter overturn or extend the verdict│├─ ::check_permissions()│ ├─ permission_callback callback the ability's own permission check│ └─ wp_ability_permission_result filter overturn or extend the verdict│├─ wp_before_execute_ability action the callback is about to run│├─ ::do_execute()│ ├─ execute_callback callback the ability does its work│ └─ wp_ability_execute_result filter transform a result or recover│├─ ::validate_output()│ ├─ schema validation built-in checked against output_schema│ └─ wp_ability_validate_output filter overturn or extend the verdict│└─ wp_after_execute_ability action validated success only
execute() calls the three actions and the wp_pre_execute_ability filter directly. Every other hook runs inside the method it sits under in the diagram, and those methods can also be called on their own. WP-CLI and the REST run controller use them that way, checking permissions or validating input without running the ability. So wp_ability_permission_result applies to those checks too, not only to the ones that lead to execution.
Placement is part of each hook’s contract, and the three actions show this most clearly. If you count executions for billing or quota, wp_ability_invoked, wp_before_execute_ability, and wp_after_execute_ability will each report a different number, and only one of them answers the question you asked.
Every attempt: wp_ability_invoked
wp_ability_invoked fires before normalization, validation, permission checks, and short-circuiting. It therefore runs for valid calls, invalid input, permission failures, cached responses, approval flows, and other early returns.
add_action( 'wp_ability_invoked', static function ( string $ability_name, $raw_input, WP_Ability $ability ): void { my_plugin_record_event( array( 'ability' => $ability_name, 'timestamp' => time(), ) ); }, 10, 3);
The second argument is deliberately raw. It may contain credentials, personal data, unpublished content, or malformed values, and nothing has inspected it yet. Avoid copying it into general-purpose logs. Record only the fields the audit or telemetry use case requires, with appropriate retention and access controls.
Imminent and completed execution
The existing wp_before_execute_ability and wp_after_execute_ability actions now include the WP_Ability object as their final argument:
add_action( 'wp_before_execute_ability', static function ( string $ability_name, $input, WP_Ability $ability ): void { // The execute callback is about to run. }, 10, 3);add_action( 'wp_after_execute_ability', static function ( string $ability_name, $input, $result, WP_Ability $ability ): void { // The callback completed and the final output passed validation. }, 10, 4);
Existing callbacks remain compatible because WordPress passes only the number of arguments requested when the callback was added. Increase accepted_args to 3 or 4 only when the callback actually needs the ability object.
The lifecycle filters
Short-circuiting before anything else
wp_pre_execute_ability is the earliest decision point after the invocation action. It can return an error, an approval request, a cached response, or any other final value without running normalization, validation, permissions, or the execute callback.
add_filter( 'wp_pre_execute_ability', static function ( $pre, string $ability_name, $input, WP_Ability $ability ) { if ( 'my-plugin/summarize-post' !== $ability_name ) { return $pre; } if ( my_plugin_has_exceeded_summary_limit() ) { return new WP_Error( 'my_plugin_rate_limited', __( 'The summary limit has been reached. Try again later.', 'my-plugin' ), array( 'status' => 429 ) ); } return $pre; }, 10, 4);
The initial $pre value is a unique WP_Filter_Sentinel object. Return that exact value unchanged to continue. Every replacement short-circuits execution, including null, false, or a different object. Plugin code normally doesn’t need to create a sentinel itself.
Short-circuiting is powerful precisely because it bypasses the rest of the pipeline, and output validation is skipped on every channel. A cached result must be scoped to the correct user and context, and the filtering callback becomes responsible for the returned value’s integrity.
Permissions are a channel-dependent story. A direct execute() call from PHP or WP-CLI skips them along with everything else, so a filter answering from cache there is answering before anyone has checked who is asking. A REST caller has already been normalized, validated, and permission-checked by the run controller before dispatch reaches execute() and this filter fires.
Use wp_ability_normalize_input when a decision needs normalized but not yet validated input, and wp_ability_validate_input when it should run only after built-in schema validation has passed. Both can also return a WP_Error that halts the pipeline. The short circuit saves that work on the direct call paths. Over REST it saves none of it, because the controller has already done it.
Normalizing input before validation
wp_ability_normalize_input runs inside WP_Ability::normalize_input(), after the method has applied the schema’s top-level default and before schema validation. It can return normalized input or a WP_Error.
That top-level default only fills in input that arrived as null. Property-level defaults declared inside an object schema are never applied, by this method or by REST sanitization, so a 'default' => 50 on a property documents the intended value for clients without populating it. The callback still has to supply it, either with a null-coalescing fallback the way the ability above handles max_words, or through this filter the way it handles locale.
This is the right place to inject execution context that the caller shouldn’t have to supply:
add_filter( 'wp_ability_normalize_input', static function ( $input, string $ability_name, WP_Ability $ability ) { if ( 'my-plugin/summarize-post' !== $ability_name || ! is_array( $input ) ) { return $input; } if ( empty( $input['locale'] ) ) { $input['locale'] = get_locale(); } return $input; }, 10, 3);
Anything this filter adds must be declared in input_schema. The schema above sets additionalProperties: false, so an undeclared key fails validation. Declaring a property also means a client can send it. That is fine for locale, which the caller may override anyway. For a value the caller must not control, such as the current user ID, set it unconditionally rather than only when it is missing. Otherwise a caller can send their own value and claim to be someone else.
Normalization should fill in context and make equivalent values consistent. It should not turn invalid data into valid data.
Filtering the permission result
wp_ability_permission_result runs after the registered permission_callback. Return true, false, or WP_Error. Other values are coerced to false.
add_filter( 'wp_ability_permission_result', static function ( $permission, string $ability_name, $input, WP_Ability $ability ) { if ( 'my-plugin/summarize-post' !== $ability_name ) { return $permission; } if ( get_option( 'my_plugin_pause_summaries' ) ) { return new WP_Error( 'my_plugin_summaries_paused', __( 'Post summaries are temporarily unavailable.', 'my-plugin' ) ); } return $permission; }, 10, 4);
This filter runs inside check_permissions(), so it applies whenever permissions are checked, not only when the ability runs. It can override a denial as well as an approval. Site-wide policy is a fair use, but granting access the ability refused is security-sensitive, so keep the result you received unless you have a clear reason to change it.
Permission errors are the one place a status is ignored. Over REST a denial always returns the standard authorization status, so put anything the caller needs into the code and message.
Transforming or recovering the result
wp_ability_execute_result runs after execute_callback and before output validation. It receives successful values and WP_Error objects, so it can transform a result or recover from a known failure.
add_filter( 'wp_ability_execute_result', static function ( $result, string $ability_name, $input, WP_Ability $ability ) { if ( 'my-plugin/summarize-post' !== $ability_name ) { return $result; } if ( is_wp_error( $result ) && 'summary_service_unavailable' === $result->get_error_code() ) { $cached = my_plugin_get_cached_summary( $input['post_id'], $input['locale'] ); if ( null !== $cached ) { return $cached; } } return $result; }, 10, 4);
Any recovered or transformed value still passes through output validation, which makes this hook safer for fallbacks than the pre-execution short circuit. Note that the $input this filter receives is the normalized input, so the recovery path can rely on the same locale the callback used.
Validation JSON Schema can’t express
WordPress 7.1 adds wp_ability_validate_input and wp_ability_validate_output after built-in schema validation. Each receives the current validation result (true or WP_Error), the value, and the ability name.
The useful cases are rules that depend on site state, and therefore can’t live in a static schema:
add_filter( 'wp_ability_validate_input', static function ( $is_valid, $input, string $ability_name ) { if ( 'my-plugin/summarize-post' !== $ability_name || is_wp_error( $is_valid ) ) { return $is_valid; } $ceiling = (int) get_option( 'my_plugin_max_summary_words', 200 ); if ( isset( $input['max_words'] ) && $input['max_words'] > $ceiling ) { return new WP_Error( 'my_plugin_summary_too_long', sprintf( /* translators: %d: maximum number of words configured for the site. */ __( 'This site limits summaries to %d words.', 'my-plugin' ), $ceiling ), array( 'status' => 422 ) ); } return true; }, 10, 3);
The schema’s static maximum of 200 still defines the outer bound published to clients. The filter narrows it to whatever the site has configured, which is information a portable schema can’t carry.
The output filter is the final integrity gate before wp_after_execute_ability:
add_filter( 'wp_ability_validate_output', static function ( $is_valid, $output, string $ability_name ) { if ( 'my-plugin/summarize-post' !== $ability_name || is_wp_error( $is_valid ) ) { return $is_valid; } if ( '' === trim( wp_strip_all_tags( $output['summary'] ) ) ) { return new WP_Error( 'my_plugin_empty_summary', __( 'The generated summary contains no readable text.', 'my-plugin' ) ); } return true; }, 10, 3);
Return the result you were given, replace it with your own WP_Error, or return true to accept data the built-in check rejected. Returning false rejects the value too, but WordPress then substitutes a generic error. A WP_Error is better, because its code and message reach the caller, along with a custom status if you set one. Otherwise the caller gets 400.
Keep authorization in permission_callback or wp_ability_permission_result. Input validation runs before permissions, so a validation error must not reveal protected state. “This site limits summaries to 200 words” is safe. “Post 41 is in the private drafts category” is not.
Both edge cases involve a missing schema. Without an input_schema, only null input succeeds and anything else fails with ability_missing_input_schema before the filter runs. wp_ability_validate_output is more forgiving and runs even with no output_schema, but declare both so clients can discover the contract.
Schemas as public contracts
An ability schema serves two related but distinct jobs. The canonical schema stored on WP_Ability drives WordPress’s server-side validation. A client-facing copy describes the contract to REST, WP-CLI, JavaScript, MCP, AI clients, and other consumers.
Declaring types precisely pays off more than it used to. When an ability runs through the Abilities REST API with GET or DELETE, its input travels in the query string, where every value starts out as a string. In 7.1 the run controller converts those values to the types the schema declares before the permission and execute callbacks run, so an integer property arrives as 41 and not "41". Conversion never rescues invalid input, because a value is converted only when validation already accepts it, and direct PHP callers still have to pass the declared types themselves.
Write the canonical schema as standard JSON Schema wherever possible. WordPress has long accepted a property-level required flag:
'post_id' => array( 'type' => 'integer', 'required' => true,),
Put the required property names in the parent’s required array instead, and give every property a description:
'input_schema' => array( 'type' => 'object', 'properties' => array( 'post_id' => array( 'type' => 'integer', 'description' => __( 'Post to summarize.', 'my-plugin' ), ), ), 'required' => array( 'post_id' ),),
That description is the part of the schema a model reads, and the only place to say what the key can’t. Use it for the unit, the default, or the fallback a caller would otherwise have to guess. A client meeting the ability for the first time has nothing else to go on.
A well-written canonical schema still can’t be sent to clients unchanged. A 'required' => true flag on a property is not valid Draft 4 JSON Schema, and the sanitize_callback and validate_callback entries familiar from REST route schemas mean nothing to a client that doesn’t run PHP. WordPress 7.1 adds wp_prepare_json_schema_for_client() for the second job. It makes a copy for clients and leaves the canonical schema alone. The copy is built from an allowlist, so any schema keyword the profile doesn’t recognize is dropped along with them.
$ability = wp_get_ability( 'my-plugin/summarize-post' );if ( $ability ) { $client_schema = wp_prepare_json_schema_for_client( $ability->get_input_schema(), 'draft-04' );}
WordPress applies this preparation automatically to schemas in Abilities REST API responses and to ability input schemas converted into WP AI Client function declarations. Call the helper yourself when publishing a schema through a custom REST route, JavaScript configuration, MCP adapter, file, or another integration.
Don’t overwrite the schema stored on the ability with the prepared result. Server-side validation may intentionally understand WordPress-specific behavior that shouldn’t be sent to a general client.
Canonical and client-facing representations
This is the compatibility consequence worth planning for. WP_Ability::get_input_schema() and get_output_schema() continue to return the original arrays to server-side PHP. REST responses carry prepared input and output schemas, and WP AI Client function declarations carry the prepared input schema. Comparing a canonical schema with its client-facing counterpart will show intentional differences, and any code that assumed they were identical needs revisiting.
Concretely, an integration that snapshots ability schemas over REST and diffs them across releases will see one-time churn at 7.1, from the transformations described in the next section applied to schemas that previously went out untouched. None of that changes server-side validation behavior. Tests that assert on the exact serialized shape of a REST schema response will need updating. Tests that assert on validation behavior won’t.
What client preparation changes
wp_prepare_json_schema_for_client() walks the complete schema recursively, so nested properties, array items, definitions, dependencies, and composition keywords all receive the same treatment. Depending on the selected profile, it:
- keeps allowed JSON Schema keywords and removes unknown or server-only ones
- strips WordPress artifacts such as
sanitize_callback,validate_callback, andarg_options - moves property-level
required => trueflags into a Draft 4requiredarray on the parent object - removes
required => falseand scalar boolean forms that have no Draft 4 equivalent - preserves an existing
requiredarray when both forms are present - converts an empty-array default on an object schema so that JSON serializes it as
{}.
Two profiles are available. draft-04 is the default and the broader of the two, preserving composition and reference keywords such as $ref, definitions, allOf, not, dependencies, and additionalItems. Use it for standalone schemas aimed at general-purpose clients. rest-api uses the narrower historical keyword set that applies when a schema is embedded in WordPress REST route definitions.
wp_get_json_schema_allowed_keywords( $profile ) returns the keyword allowlist, and the wp_json_schema_allowed_keywords filter can modify it for a profile. Allowing a keyword in serialized output doesn’t teach WordPress how to validate or sanitize against it, so extend the list only when the receiving clients are known to understand the addition.
Prepared Draft 4 is a portable baseline, not a provider-specific strict tool schema. If a model provider supports only a smaller JSON Schema dialect, adapt the prepared schema in that provider’s integration layer. Making that adaptation a first-class per-provider concern is tracked in php-ai-client.
Schema callbacks aren’t ability callbacks
Don’t put REST-style validate_callback or sanitize_callback entries inside an ability’s input or output schema and expect WP_Ability to run them. It doesn’t, and 7.1 now strips them from the client-facing copy as well, so they are neither executed nor published.
Normalize equivalent input representations in wp_ability_normalize_input. Add input business validation in wp_ability_validate_input. Authorize in permission_callback or wp_ability_permission_result. Add output invariants in wp_ability_validate_output. This separation also keeps the published schema serializable and useful outside PHP.
One source of truth
WordPress 7.1 doesn’t force every integration through one protocol. Instead, it makes the ability definition, the execution pipeline, the schema contract, and the discovery query consistent enough that PHP, REST, WP-CLI, JavaScript, MCP, and AI-facing tools can build on the same source of truth. The parts I expect to age best are the ones that moved policy out of individual abilities and into the pipeline: a site can now express what may run, what may be discovered, and what may be published without every plugin having to agree in advance.
If you’re building on the Abilities API, whether through an MCP adapter, an agent loop, or a plugin exposing abilities of its own, I’d like to hear which extension points you reach for and which ones still feel missing.
Further reading
- New execution lifecycle filters for the Abilities API in WordPress 7.1
- Abilities API improvements in WordPress 7.1
- JSON Schema preparation for client compatibility in WordPress 7.1
- A unified public exposure flag for Abilities in WordPress 7.1
- Filtering registered abilities with wp_get_abilities() in WordPress 7.1
- Abilities API handbook
wp_register_ability()referencewp abilityWP-CLI command

Leave a Reply