Filtering registered abilities with wp_get_abilities() in WordPress 7.1

WordPress 7.1 extends wp_get_abilities() with a standard way to filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. registered abilities.

The function now accepts an optional $args array that can filter abilities by category, namespace, or metadata. It also supports callbacks for custom per-item filtering and final result processing.

Two new WordPress filters allow plugins to influence ability retrieval across the site:

  • wp_get_abilities_item_include
  • wp_get_abilities_result

The REST APIREST API The REST API is an acronym for the RESTful Application Program Interface (API) that uses HTTP requests to GET, PUT, POST and DELETE data. It is how the front end of an application (think “phone app” or “website”) can communicate with the data store (think “database” or “file system”) https://developer.wordpress.org/rest-api/’s abilities list controller now uses wp_get_abilities() instead of implementing categoryCategory The 'category' taxonomy lets you group posts / content together that share a common bond. Categories are pre-defined and broad ranging. filtering separately. It also supports filtering abilities by namespace.

Why was this change needed?

Before WordPress 7.1, there were two ways to retrieve abilities:

// Retrieve every registered ability.
$abilities = wp_get_abilities();

// Retrieve one named ability.
$ability = wp_get_ability( 'my-plugin/export-users' );

wp_get_abilities() always returned the complete registry. A caller needing a subset had to retrieve every ability and filter the result manually:

$abilities = array_filter(
	wp_get_abilities(),
	function ( WP_Ability $ability ): bool {
		return 'data-export' === $ability->get_category();
	}
);

Several consumers developed their own versions of this pattern for category, namespace, and metadata checks. The REST abilities controller also performed its own category filtering after retrieving the complete registry.

This led to:

  • Duplicated filtering code.
  • Inconsistent filtering semantics between consumers.
  • Different behaviour between the PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 7.4 or higher and REST APIs.
  • No standard extension points for ability selection.
  • Additional filtering passes over the registry.

WordPress 7.1 moves this work into wp_get_abilities(), providing one shared filtering pipeline for CoreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. and plugins.

Filtering by category

Pass a category slug using the category argument:

$abilities = wp_get_abilities(
	array(
		'category' => 'data-export',
	)
);

The comparison is exact. Only abilities whose category exactly matches the supplied string are returned.

The category must be passed as a single string. Arrays of category slugs are not supported.

Filtering by namespace

Use namespace to retrieve abilities registered under a particular namespace:

$abilities = wp_get_abilities(
	array(
		'namespace' => 'my-plugin',
	)
);

The namespace is passed without the trailing slash. Both of the following values are normalised to the same namespace:

'namespace' => 'my-plugin',
'namespace' => 'my-plugin/',

An ability such as my-plugin/export-users matches, while another-plugin/export-users does not.

Namespace matching includes the namespace delimiter. Passing my-plugin does not accidentally match an ability registered under a similarly named my-plugin-extra namespace.

Filtering by metadata

The meta argument selects abilities whose metadata contains the specified key-value pairs:

$abilities = wp_get_abilities(
	array(
		'meta' => array(
			'public' => true,
		),
	)
);

All supplied metadata conditions must match:

$abilities = wp_get_abilities(
	array(
		'meta' => array(
			'public'       => true,
			'show_in_rest' => true,
		),
	)
);

Nested metadata is supported:

$abilities = wp_get_abilities(
	array(
		'meta' => array(
			'my_client' => array(
				'public' => true,
			),
		),
	)
);

Metadata comparisons are strict. The value true does not match 1, and false does not match 0.

The metadata filter checks that every requested condition exists and matches. An ability may contain additional metadata that was not included in the query.

Combining declarative filters

The category, namespace, and meta arguments can be combined:

$abilities = wp_get_abilities(
	array(
		'category'  => 'data-export',
		'namespace' => 'my-plugin',
		'meta'      => array(
			'public' => true,
		),
	)
);

Conditions are combined using AND logic. An ability must satisfy every supplied argument to be included.

In this example, the result contains only abilities that:

  • Belong to the data-export category.
  • Use the my-plugin namespace.
  • Have resolved public metadata set to true.

Custom per-item filtering

Conditions that cannot be expressed using the declarative arguments can be handled with item_include_callback:

$abilities = wp_get_abilities(
	array(
		'namespace'             => 'my-plugin',
		'item_include_callback' => function (
			WP_Ability $ability
		): bool {
			return my_plugin_should_include_ability( $ability );
		},
	)
);

The callback runs once for every ability that passed the declarative filters. It receives the WP_Ability instance and must return a boolean:

  • Return true to include the ability.
  • Return false to exclude it.

This callback is scoped to the current wp_get_abilities() call. It does not affect ability retrieval elsewhere.

Use it for conditions such as:

  • Custom metadata relationships.
  • Context-dependent visibility.
  • Integration-specific rules.
  • Conditions involving more than one ability property.

Processing the complete result

Use result_callback when an operation requires the complete matched array:

$abilities = wp_get_abilities(
	array(
		'namespace'       => 'my-plugin',
		'result_callback' => function ( array $abilities ): array {
			uasort(
				$abilities,
				function (
					WP_Ability $first,
					WP_Ability $second
				): int {
					return strcasecmp(
						$first->get_label(),
						$second->get_label()
					);
				}
			);

			return array_slice(
				$abilities,
				0,
				10,
				true
			);
		},
	)
);

The result callback runs after all per-item matching has completed. It is suitable for:

  • Sorting.
  • Slicing or pagination.
  • Reordering.
  • Other final result transformations.

Like item_include_callback, result_callback applies only to the current function call.

Registered abilities are normally returned in an associative array keyed by ability name. When sorting or slicing the result, preserve those keys when downstream code depends on them.

New global filters

WordPress 7.1 also introduces two filters for plugins that need to affect ability retrieval beyond a single call site.

wp_get_abilities_item_include

The wp_get_abilities_item_include filter runs for every ability that passed the declarative conditions and the caller’s item_include_callback:

add_filter(
	'wp_get_abilities_item_include',
	function (
		bool $include,
		WP_Ability $ability,
		array $args
	): bool {
		if ( 'my-plugin/private-operation' === $ability->get_name() ) {
			return false;
		}

		return $include;
	},
	10,
	3
);

The filter receives:

  • $include: Whether the ability should currently be included.
  • $ability: The ability being evaluated.
  • $args: The complete arguments passed to wp_get_abilities().

Because declarative mismatches are removed before this filter runs, the filter cannot add an ability that failed category, namespace, or meta matching. It can influence the inclusion of abilities that have reached this stage, as well as their global exclusion.

wp_get_abilities_result

The wp_get_abilities_result filter receives the complete result after the caller’s result_callback:

add_filter(
	'wp_get_abilities_result',
	function ( array $abilities, array $args ): array {
		// Apply site-wide result processing when appropriate.
		return $abilities;
	},
	10,
	2
);

The filter receives:

  • $abilities: The final matched array.
  • $args: The complete arguments passed to wp_get_abilities().

It can be used for site-wide sorting, reordering, or other final processing.

These are global filters. Plugins should use them only when the behaviour is intended to affect every relevant caller. For logic that belongs to one operation, prefer item_include_callback or result_callback.

Filtering order

The complete pipeline runs in the following order:

  1. Match the category argument.
  2. Match the namespace argument.
  3. Match the meta argument.
  4. Run item_include_callback.
  5. Apply wp_get_abilities_item_include.
  6. Add included abilities to the matched result.
  7. Run result_callback on the complete result.
  8. Apply wp_get_abilities_result.

The declarative checks, item callback, and item filter run within a single pass over the registry.

This avoids the separate array_filter() passes that consumers previously had to implement.

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-plugin
GET /wp-json/wp-abilities/v1/abilities?category=my-plugin-content
GET /wp-json/wp-abilities/v1/abilities?meta[annotations][readonly]=true

Parameters can be combined and use the same AND logic.

?category=data-export&namespace=my-plugin

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, the REST API casts "true" to a boolean true before the value reaches the metadata-matching logic.

GET /wp-json/wp-abilities/v1/abilities?meta[my_plugin][enabled]=true

Built-in annotation types

The known readonly, destructive, and idempotent annotation values are coerced from query strings to boolean values before strict matching. Core declares the schema for these standard ability annotations.

Each accepts a boolean or null, so REST can correctly cast their query values without a pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party. extending the schema:

?meta[annotations][readonly]=true

Use rest_abilities_collection_params filter when making additional metadata fields queryable, especially boolean, integer, number, array, or object values that cannot be matched correctly as untyped query strings.

Backward compatibility

The $args parameter is optional:

$abilities = wp_get_abilities();

Existing calls remain valid, and the function still returns an array of WP_Ability instances keyed by ability name.

Code that manually filters the result can continue to work:

$abilities = array_filter(
	wp_get_abilities(),
	'my_plugin_filter_abilities'
);

However, plugins should migrate common category, namespace, and metadata checks to the new arguments. Doing so reduces duplicated code and allows Core and other integrations to use consistent matching behaviour.

One behavioural detail deserves particular attention: the two new global filters run even when wp_get_abilities() is called without arguments.

As a result, the following call now means “retrieve abilities through the standard filtering pipeline”:

$abilities = wp_get_abilities();

It does not necessarily mean “retrieve raw registry contents,” because another plugin can alter the result through wp_get_abilities_item_include and wp_get_abilities_result.

Retrieving the raw registry

Code that specifically needs the complete, unfiltered registry can use WP_Abilities_Registry::get_all_registered():

$registry  = WP_Abilities_Registry::get_instance();
$abilities = $registry->get_all_registered();

This bypasses:

  • Declarative filtering.
  • Caller callbacks.
  • wp_get_abilities_item_include
  • wp_get_abilities_result

Most application and integration code should continue using wp_get_abilities(). Direct registry access is appropriate only when raw registered state is explicitly required, such as low-level debugging or registry inspection.

Filtering does not replace authorisation

Filtering controls which abilities are returned during discovery. It does not determine whether the current user may execute an ability.

An ability’s permission_callback remains responsible for authorisation:

'permission_callback' => function (): bool {
	return current_user_can( 'manage_options' );
},

Developers should not assume that an ability returned by wp_get_abilities() is executable by the current user.

Similarly, excluding an ability from a filtered result is not a security boundary. Any sensitive operation must enforce its permissions when the ability is executed.

When to use each option

Use declarative arguments for standard selection:

wp_get_abilities(
	array(
		'category'  => 'data-export',
		'namespace' => 'my-plugin',
		'meta'      => array(
			'public' => true,
		),
	)
);

Use item_include_callback for custom conditions that apply to one call:

wp_get_abilities(
	array(
		'item_include_callback' => 'my_plugin_should_include_ability',
	)
);

Use result_callback for call-specific sorting or slicing:

wp_get_abilities(
	array(
		'result_callback' => 'my_plugin_prepare_ability_results',
	)
);

Use wp_get_abilities_item_include or wp_get_abilities_result only for behaviour intended to affect ability retrieval across callers.

Use WP_Abilities_Registry::get_all_registered() only when code explicitly requires raw, unfiltered registry data.

Together, these changes make wp_get_abilities() the shared discovery and filtering primitive for the Abilities APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways., replacing duplicated filtering implementations with a consistent, extensibleExtensible This is the ability to add additional functionality to the code. Plugins extend the WordPress core software. pipeline.

These changes were introduced in changeset [62420] for TracTrac An open source project by Edgewall Software that serves as a bug tracker and project management tool for WordPress. ticketticket Created for both bug reports and feature development on the bug tracker. #64990.

Props to @benjamin_zekavica for peer review, and @gziolo for review, technical guidance, and suggested improvements.

#abilities-api, #7-1, #dev-notes, #dev-notes-7-1, #rest-api

A unified public exposure flag for Abilities in WordPress 7.1

WordPress 7.1 introduces a new public metadata flag for abilities. The flag provides a single, high-level way to indicate that an ability is intended to be available to external clients such as the REST APIREST API The REST API is an acronym for the RESTful Application Program Interface (API) that uses HTTP requests to GET, PUT, POST and DELETE data. It is how the front end of an application (think “phone app” or “website”) can communicate with the data store (think “database” or “file system”) https://developer.wordpress.org/rest-api/, MCP adapters, and AI agents.

Table of contents:

Previously, ability authors had to express that intent separately for every exposure channel. For example, an ability exposed through the REST API needed to set show_in_rest directly:

'meta' => array(
	'show_in_rest' => true,
),

As the Abilities APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. gains more client integrations, repeating the same intent through several channel-specific flags becomes difficult to maintain. The new public flag establishes a common default while preserving granular control for each channel.

Registering a public ability

An ability intended for client exposure can now set meta.public when it is registered:

function my_plugin_register_abilities(): void {
	wp_register_ability(
		'my-plugin/export-users',
		array(
			'label'               => __( 'Export users', 'my-plugin' ),
			'description'         => __( 'Exports user data as CSV.', 'my-plugin' ),
			'category'            => 'data-export',
			'execute_callback'    => 'my_plugin_export_users',
			'permission_callback' => function (): bool {
				return current_user_can( 'export' );
			},
			'meta'                => array(
				'public' => true,
			),
		)
	);
}

add_action( 'wp_abilities_api_init', 'my_plugin_register_abilities' );

For the REST API, setting public to true makes show_in_rest default to true. The ability can therefore be discovered and invoked through the REST abilities endpoints, subject to its permission callback.

How exposure defaults are resolved

Channel-specific settings take precedence over the general public setting. The effective REST exposure value is resolved as follows:

$show_in_rest = $meta['show_in_rest'] ?? $meta['public'] ?? false;

For example, an ability that is generally public but must not be exposed through REST can use:

'meta' => array(
	'public'       => true,
	'show_in_rest' => false,
),

The explicit show_in_rest value wins over public.

Conversely, an ability that is not generally public can still opt into REST specifically:

'meta' => array(
	'public'       => false,
	'show_in_rest' => true,
),

The resolution uses null-coalescing semantics so an explicit false is preserved. It is not treated as a missing value.

A null value is treated as unset and falls back to the next value in the chain.

In practical terms:

Registration metadataEffective publicEffective show_in_rest
No exposure metadatafalsefalse
public => truetruetrue
public => falsefalsefalse
show_in_rest => truefalsetrue
public => true, show_in_rest => falsetruefalse
public => false, show_in_rest => truefalsetrue

This precedence allows for a broad exposure default while opting in or out of individual channels.

What problem does this change fix?

Ability metadata already contained channel-specific exposure settings such as show_in_rest. As support for MCP, AI agents, and other clients develops, requiring ability authors to configure each channel independently would duplicate the same policy across multiple properties:

'meta' => array(
	'show_in_rest' => true,
	'mcp'          => array(
		'public' => true,
	),
	// Additional flags for future clients.
),

This also makes it difficult for a newly introduced channel to determine whether an existing ability was intended for external use.

The public flag fixes this by recording the ability author’s general exposure intent in one stable location:

'meta' => array(
	'public' => true,
),

Individual integrations can use that value as their default while retaining a more specific channel-level override.

REST is the first built-in consumer of this behaviour. Other integrations can adopt the same default without adding channel-specific logic to WordPress CoreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress..

Using public in other integrations

The resolved public value remains available in the ability’s metadata. Client integrations can inspect this value when determining whether an ability should be exposed.

The WordPress MCP Adapter will respect the unified public flag starting with its next release. WP-CLIWP-CLI WP-CLI is the Command Line Interface for WordPress, used to do administrative and development tasks in a programmatic way. The project page is http://wp-cli.org/ https://make.wordpress.org/cli/ does not apply this exposure check because its ability-listing functionality returns all registered abilities.

Other integrations should generally resolve exposure when abilities are selected for that integration:

function my_plugin_is_ability_exposed(
	WP_Ability $ability,
	string $channel
): bool {
	$meta = $ability->get_meta();

	return $meta[ $channel ]['public'] ?? $meta['public'] ?? false;
}

Integrations that need to derive their own channel-specific metadata during registration can use the existing wp_register_ability_args filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output.:

add_filter(
	'wp_register_ability_args',
	function ( array $args, string $name ): array {
		if (
			! isset( $args['meta']['my_client']['public'] )
			&& isset( $args['meta']['public'] )
		) {
			$args['meta']['my_client']['public'] =
				(bool) $args['meta']['public'];
		}

		return $args;
	},
	10,
	2
);

An integration should follow the same precedence rule as REST:

  1. Use an explicit channel-specific value when present.
  2. Otherwise, inherit public.
  3. Otherwise, use the channel’s built-in default.

Integrations should not overwrite an explicit channel opt-out merely because public is true.

Exposure is not authorisation

The public flag controls discoverability and client exposure. It does not make an ability executable without authorisation, and it does not replace the ability’s permission_callback.

Every ability must continue to implement an appropriate permission check:

'permission_callback' => function (): bool {
	return current_user_can( 'manage_options' );
},

An ability with public => true may be visible through a client while still requiring authentication and specific WordPress capabilitiescapability A capability is permission to perform one or more types of task. Checking if a user has a capability is performed by the current_user_can function. Each user of a WordPress site might have some permissions but not others, depending on their role. For example, users who have the Author role usually have permission to edit their own posts (the “edit_posts” capability), but not permission to edit other users’ posts (the “edit_others_posts” capability). to execute.

Developers should not treat public, show_in_rest, or any other exposure flag as a security boundary. Authorisation must be enforced by the ability itself.

Changes to resolved metadata

In WordPress 7.1, the resolved metadata for every ability includes a boolean public property. It defaults to false when it is not supplied during registration.

For example:

$ability = wp_get_ability( 'my-plugin/export-users' );
$meta    = $ability->get_meta();

$is_public = $meta['public']; // Always a boolean in WordPress 7.1.

This gives consumers a consistent value to inspect without having to test whether the key exists.

The new property is also declared in the REST API’s ability metadata schema, allowing REST clients to inspect the general exposure intent.

Backward compatibility

The change does not alter the signature or return value of wp_register_ability() or other Abilities API functions.

Existing channel-specific registrations continue to work:

'meta' => array(
	'show_in_rest' => true,
),

An explicit show_in_rest value remains authoritative. Plugins are not required to replace it with public.

Abilities that previously supplied neither public nor show_in_rest remain unavailable through REST. Their resolved metadata now contains public => false, but their exposure behaviour is unchanged.

Developers should consider migrating from show_in_rest => true to public => true when an ability is generally intended for use by multiple client types. Continue using show_in_rest directly when exposure is intentionally limited to REST or when overriding the general policy.

Existing Core abilities

The following abilities included with WordPress now use meta.public instead of setting meta.show_in_rest directly:

  • core/get-site-info
  • core/get-user-info
  • core/get-environment-info

Their REST availability has not changed. Because public => true supplies the default for show_in_rest, these abilities remain exposed through REST as before.

Using the high-level flag also allows other client integrations to recognise that these Core abilities are intended for external use.

When to use each flag

Use public when the ability is generally intended for consumption by external clients.

Use a channel-specific flag when:

  • The ability should be exposed through only that channel.
  • The ability needs to opt out of a channel despite being generally public.
  • A client integration provides behaviour that cannot be represented by the general flag.

The change was introduced in changeset [62729], with Core abilities migrated in changeset [62737]. See TracTrac An open source project by Edgewall Software that serves as a bug tracker and project management tool for WordPress. ticketticket Created for both bug reports and feature development on the bug tracker. #65568 for the complete discussion.

Props to @gziolo and @benjamin_zekavica for peer review.

#abilities-api, #7-1, #dev-notes, #dev-notes-7-1, #rest-api

JSON Schema preparation for client compatibility in WordPress 7.1

WordPress 7.1 introduces a shared JSONJSON JSON, or JavaScript Object Notation, is a minimal, readable format for structuring data. It is used primarily to transmit data between a server and web application, as an alternative to XML. Schema preparation layer for schemas exposed to REST clients, frontend applications, and AI tools.

WordPress accepts several internal schema conventions that are useful during server-side validation but are not portable JSON Schema draft-04. Passing these schemas directly to external validators could cause validation errors or expose PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 7.4 or higher callbacks and other server-only implementation details.

The wp_prepare_json_schema_for_client() function

The new wp_prepare_json_schema_for_client() function converts a WordPress schema into a portable, client-facing representation before it is exposed to REST clients, frontend applications, AI tools, or other external consumers.

Most developers do not need to take action. CoreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. now applies this preparation automatically to:

  • Abilities APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. schemas exposed through REST responses.
  • Ability input schemas converted into AI Client function declarations.
$prepared_schema = wp_prepare_json_schema_for_client( $schema );

This keeps the schemas used by clients (REST clients, JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com applications, and AI tools) consistent. See ticketticket Created for both bug reports and feature development on the bug tracker. #64955 and changeset [62591].

Compatibility guidance

This change is primarily automatic. Existing ability registration and execution code does not need to call the new function.

Call wp_prepare_json_schema_for_client() directly when a pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party. exposes a WordPress-style schema outside the server-side PHP validation boundary, for example through:

  • a custom REST endpoint;
  • a JavaScript configuration object;
  • an MCP tool declaration;
  • an AI function declaration; or
  • another external schema consumer.

Do not replace the schema stored by an ability with the prepared version. Keep the canonical WordPress schema for server-side use and prepare a copy only when sending it to a client.

Choosing a schema profile

This new function accepts a schema and an optional schema profile:

/** 
 * Prepares a JSON Schema for clients.
 * 
 * @param array<string, mixed> $schema         The schema array.
 * @param string               $schema_profile Optional. Name of the schema
 *                                             profile whose keywords should be
 *                                             preserved. Default 'draft-04'.
 * @return array<string, mixed> The prepared schema.
 */
wp_prepare_json_schema_for_client(
	array $schema,
	string $schema_profile = 'draft-04'
): array

WordPress provides two schema profiles out of the box:

  • draft-04 is the default. Use it when publishing a standalone schema to general-purpose clients, including Ability metadata, frontend validators, MCP integrations, and AI tooling. It preserves the broader JSON Schema Draft 4 vocabulary, including composition and reference keywords such as $ref, definitions, allOf, not, dependencies, and additionalItems.
  • rest-api uses the narrower keyword set supported by WordPress REST APIREST API The REST API is an acronym for the RESTful Application Program Interface (API) that uses HTTP requests to GET, PUT, POST and DELETE data. It is how the front end of an application (think “phone app” or “website”) can communicate with the data store (think “database” or “file system”) https://developer.wordpress.org/rest-api/ route schemas. Use it when preparing a schema that must follow the same conventions as a REST route’s argument or response schema.
// General client-facing or Ability schema.
$prepared_schema = wp_prepare_json_schema_for_client( $schema );

// Schema intended to match WordPress REST API conventions.
$prepared_rest_schema = wp_prepare_json_schema_for_client(
	$schema,
	'rest-api'
);

Both profiles produce JSON Schema Draft 4 output. The difference is the set of keywords retained in the prepared schema.

What is JSON Schema Draft 4?

“Draft 4” refers to the fourth published draft of the JSON Schema specification, which defines a JSON-based contract for describing and validating JSON data. See the JSON Schema Draft 4 core specification for its terminology and behaviour.

Schema transformations

Preparation is recursive and applies to nested object properties, array items, composition keywords, definitions, dependencies, and other subschemas.

Required properties use Draft 4 syntax

WordPress schemas may mark individual properties as required:

'properties' => array(
	'title' => array(
		'type'     => 'string',
		'required' => true,
	),
)

The prepared schema moves those property names into the containing object’s Draft 4 required array:

'required' => array( 'title' ),

The property-level boolean is then removed.

If the object already has a valid required array, that array takes precedence over property-level boolean values. A property-level required => false is removed without creating an empty required array.

A boolean required value on a scalar schema is also removed because it has no Draft 4 equivalent.

This preparation only affects schemas sent to clients. It does not change the server-side behaviour of rest_validate_value_from_schema() or WP_Ability::validate_input().

Example of transforming schema:

$schema = array(
	'type'       => 'object',
	'properties' => array(
		'title'   => array(
			'type'              => 'string',
			'required'          => true,
			'sanitize_callback' => 'sanitize_text_field',
		),
		'content' => array(
			'type'              => 'string',
			'validate_callback' => 'is_string',
		),
	),
);

$prepared_schema = wp_prepare_json_schema_for_client( $schema );

The prepared schema is equivalent to:

array(
	'type'       => 'object',
	'required'   => array( 'title' ),
	'properties' => array(
		'title'   => array(
			'type' => 'string',
		),
		'content' => array(
			'type' => 'string',
		),
	),
);

The original $schema is not modified.

Server-only keywords are removed

PHP callbacks and other WordPress-specific keywords cannot be represented meaningfully in JSON. The preparation process removes unsupported keywords, including:

  • sanitize_callback
  • validate_callback
  • arg_options

These keywords are removed recursively, including when they appear inside:

  • properties
  • patternProperties
  • definitions
  • dependencies
  • items
  • additionalItems
  • additionalProperties
  • anyOf
  • oneOf
  • allOf
  • not

The callbacks remain available in the original server-side schema. They are removed only from its client-facing representation.

Ability authors should also note that validate_callback and sanitize_callback are not executed by the Abilities API’s runtime validation. Custom ability validation should use the wp_ability_validate_input and wp_ability_validate_output filters introduced in WordPress 7.1.

Empty object defaults are represented as objects

In PHP, an empty array serializes to [], even when its schema declares an object:

array(
	'type'    => 'object',
	'default' => array(),
)

For client-facing schemas, the empty default is prepared so that JSON serialization produces an object:

{
	"type": "object",
	"default": {}
}

This prevents client validators from rejecting the default because its serialized type does not match the declared object type.

Allowed keywords filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output.

wp_prepare_json_schema_for_client() uses wp_get_json_schema_allowed_keywords() to decide which keywords to preserve.

The broader draft-04 profile can preserve composition and documentation keywords such as:

  • $ref
  • definitions
  • allOf
  • not
  • dependencies
  • additionalItems

Preserving a keyword means that it may be included in the client-facing schema. It does not mean that WordPress validates or sanitizes values against that keyword on the server.

The allowed keyword list is filterable:

add_filter(
	'wp_json_schema_allowed_keywords',
	function ( $keywords, $schema_profile ) {
		if ( 'draft-04' === $schema_profile ) {
			$keywords[] = 'x-example-keyword';
		}

		return $keywords;
	},
	10,
	2
);

Custom keywords should only be exposed when the receiving clients are known to understand them.

Abilities REST API changes

WordPress now prepares an ability’s input and output schemas before including them in Abilities REST API responses.

For abilities registered with REST visibility, consumers of endpoints under:

/wp-json/wp-abilities/v1/abilities

receive portable schemas instead of the original WordPress-internal schema arrays.

This is an output-boundary transformation:

  • WP_Ability::get_input_schema() and WP_Ability::get_output_schema() continue to return the original schemas to server-side PHP.
  • REST responses contain prepared versions.
  • Ability execution and server-side validation remain unchanged.

Developers comparing a schema retrieved directly from a WP_Ability object with one returned through REST may therefore see intentional differences.

AI Client integration

When the WordPress AI Client converts abilities into function declarations, it now prepares each ability’s input schema first:

$input_schema = wp_prepare_json_schema_for_client(
	$ability->get_input_schema()
);

This prevents WordPress-only schema keywords and nonportable required conventions from being passed directly into AI function declarations.

The helper prepares a portable Draft 4 schema; it is not a provider-specific compiler. Individual AI providers may support a smaller schema vocabulary or impose additional requirements. Provider-specific adaptation may therefore still occur elsewhere in the integration.

Follow-up

Provider-specific adaptation is tracked in WordPress/php-ai-client#256: each provider should be able to override the input schema for its own API requirements, which differ across providers and evolve over time.

  • #64955 — Add schema compiler for AI tool calling compatibility
  • Changeset [62591] — Abilities API: Reuse JSON Schema client preparation
  • Changeset [62549] — REST API: Add a shared helper for JSON Schema allowed keywords.
  • Changeset [62449] — Abilities API: Normalize required schema shape for REST responses 

Props to @jorbin for peer review and suggested improvements.

#abilities-api, #7-1, #dev-notes, #dev-notes-7-1, #rest-api

Abilities API improvements in WordPress 7.1

WordPress 7.1 expands the Abilities APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. with custom validation hooksHooks In WordPress theme and development, hooks are functions that can be applied to an action or a Filter in WordPress. Actions are functions performed when a certain event occurs in WordPress. Filters allow you to modify certain functions. Arguments used to hook both filters and actions look the same., a new invocation lifecycle action, richer user information, selective field responses, and more consistent schemas for the coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. abilities introduced in WordPress 6.9.

Custom input and output validation

WP_Ability validates input and output against each ability’s JSONJSON JSON, or JavaScript Object Notation, is a minimal, readable format for structuring data. It is used primarily to transmit data between a server and web application, as an alternative to XML. Schema. WordPress 7.1 adds two filters that let extenders supplement that validation:

  • wp_ability_validate_input
  • wp_ability_validate_output

Each filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. 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 pluginPlugin A plugin is a piece of software containing a group of functions that can be added to a WordPress website. They can extend functionality or add new features to your WordPress websites. WordPress plugins are written in the PHP programming language and integrate seamlessly with WordPress. These can be free in the WordPress.org Plugin Directory https://wordpress.org/plugins/ or can be cost-based plugin from a third-party. 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 invalidinvalid A resolution on the bug tracker (and generally common in software development, sometimes also notabug) that indicates the ticket is not a bug, is a support request, or is generally 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 ticketticket Created for both bug reports and feature development on the bug tracker. #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_name
  • last_name
  • nickname
  • description
  • user_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 PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 7.4 or higher 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-info
  • core/get-user-info
  • core/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 APIREST API The REST API is an acronym for the RESTful Application Program Interface (API) that uses HTTP requests to GET, PUT, POST and DELETE data. It is how the front end of an application (think “phone app” or “website”) can communicate with the data store (think “database” or “file system”) https://developer.wordpress.org/rest-api/. Like the other core abilities, it declares the new public metaMeta Meta is a term that refers to the inside workings of a group. For us, this is the team that works on internal WordPress sites like WordCamp Central and Make WordPress. 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.

#abilities-api, #7-1, #dev-notes, #dev-notes-7-1, #rest-api

New execution lifecycle filters for the Abilities API in WordPress 7.1

WordPress 7.1 introduces four filters that allow plugins to customise the execution lifecycle of abilities registered with the Abilities APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways..

The Abilities API previously provided the wp_before_execute_ability and wp_after_execute_ability actions. These actions are useful for observing execution, but they cannot change its behaviour.

The new filters allow developers to:

  • Short-circuit ability execution.
  • Transform normalised input.
  • Apply additional authorisation rules.
  • Transform or recover an execution result.

These changes were introduced in TracTrac An open source project by Edgewall Software that serves as a bug tracker and project management tool for WordPress. ticketticket Created for both bug reports and feature development on the bug tracker. #64989 and changeset [62397].

Updated execution lifecycle

The filters are applied in the following order:

wp_pre_execute_ability
        │
        ├── short-circuit when an override is returned
        ↓
WP_Ability::normalize_input()
        ↓
wp_ability_normalize_input
        ↓
WP_Ability::validate_input()
        ↓
WP_Ability::check_permissions()
        ↓
wp_ability_permission_result
        ↓
wp_before_execute_ability
        ↓
Registered execute callback
        ↓
wp_ability_execute_result
        ↓
WP_Ability::validate_output()
        ↓
wp_after_execute_ability
        ↓
Return result

Input and output transformations occur before their respective schema-validation steps. Transformed values must therefore continue to satisfy the ability’s registered schemas.

The exception is wp_pre_execute_ability, which bypasses the rest of the pipeline completely.

Short-circuiting execution with wp_pre_execute_ability

The wp_pre_execute_ability filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. runs at the beginning of WP_Ability::execute(), before input normalization, validation or permission checks.

/**
 * Filters whether to short-circuit ability execution.
 *
 * @param mixed      $pre          Precomputed result. Return it unchanged to
 *                                 continue normal execution.
 * @param string     $ability_name Name of the ability.
 * @param mixed      $input        Raw input passed to execute().
 * @param WP_Ability $ability      Ability instance.
 */
apply_filters(
    'wp_pre_execute_ability',
    $pre,
    $ability_name,
    $input,
    $ability
);

Returning $pre unchanged allows execution to continue. Returning any other value short-circuits execution and returns that value directly to the caller.

The filter uses a unique internal sentinel as its default. This means that any PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 7.4 or higher value—including null, false, or an object—can be used as a legitimate short-circuit result.

The filter can be used for caching, rate limiting, maintenance mode, approval workflows and test mocking.

Temporarily disabling an ability

The filter can short-circuit selected abilities during maintenance without running input validation, permission checks, or the registered callback:

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
);

Returning $pre unchanged continues normal execution. When maintenance mode is enabled, the WP_Error is returned immediately, and the remaining ability pipeline is bypassed.

Because this filter runs before permission checks and validation, it should make only narrow decisions that do not depend on validated input or the current ability authorisation result.

Transforming input with wp_ability_normalize_input

The wp_ability_normalize_input filter runs inside WP_Ability::normalize_input(), after the method has applied any defaults declared by the input schema.

/**
 * Filters normalized ability input.
 *
 * @param mixed      $input        Normalized input.
 * @param string     $ability_name Name of the ability.
 * @param WP_Ability $ability      Ability instance.
 */
apply_filters(
    'wp_ability_normalize_input',
    $input,
    $ability_name,
    $ability
);

This filter can be used to:

  • Add defaults that cannot be expressed through JSONJSON JSON, or JavaScript Object Notation, is a minimal, readable format for structuring data. It is used primarily to transmit data between a server and web application, as an alternative to XML. Schema.
  • Normalize incoming values.
  • Enrich an AI prompt.
  • Inject caller or execution-context metadata.

Adding contextual input

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
);

The transformed input is subsequently checked against the ability’s input_schema.

Returning a WP_Error stops execution before input validation, permission checks and the registered callback:

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
);

When execution occurs through the Abilities REST APIREST API The REST API is an acronym for the RESTful Application Program Interface (API) that uses HTTP requests to GET, PUT, POST and DELETE data. It is how the front end of an application (think “phone app” or “website”) can communicate with the data store (think “database” or “file system”) https://developer.wordpress.org/rest-api/, a WP_Error returned during normalization is now propagated by the REST controller. It defaults to HTTPHTTP HTTP is an acronym for Hyper Text Transfer Protocol. HTTP is the underlying protocol used by the World Wide Web and this protocol defines how messages are formatted and transmitted, and what actions Web servers and browsers should take in response to various commands. status 400 unless the error specifies another status, such as 422 or 429.

Filtering permission results with wp_ability_permission_result

The wp_ability_permission_result filter runs inside WP_Ability::check_permissions(), after the registered permission_callback has executed.

/**
 * Filters the result of an ability permission check.
 *
 * @param bool|WP_Error $permission   Result from permission_callback.
 * @param string        $ability_name Name of the ability.
 * @param mixed         $input        Input used for the permission check.
 * @param WP_Ability    $ability      Ability instance.
 */
apply_filters(
    'wp_ability_permission_result',
    $permission,
    $ability_name,
    $input,
    $ability
);

The filter may return:

  • true to permit execution.
  • false to deny execution.
  • A WP_Error to deny execution with a specific error and message.

Any other return value is converted to false.

Because the filter is part of check_permissions(), it also applies when permission checks are performed independently of execute(), including REST API and WP-CLIWP-CLI WP-CLI is the Command Line Interface for WordPress, used to do administrative and development tasks in a programmatic way. The project page is http://wp-cli.org/ https://make.wordpress.org/cli/ integrations.

Applying an additional authorisation policy

add_filter(
    'wp_ability_permission_result',
    function ( $permission, $ability_name, $input, $ability ) {
        if ( 'my-plugin/delete-records' !== $ability_name ) {
            return $permission;
        }

        // preserve both false and WP_Error results
        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
);

Plugins should exercise particular care with this filter because returning true can override a denial from the ability’s original permission_callback.

Transforming results with wp_ability_execute_result

The wp_ability_execute_result filter runs after the ability’s registered execution callback and before output validation.

/**
 * Filters the result returned by an ability execute callback.
 *
 * @param mixed      $result       Result returned by the execute callback,
 *                                 or WP_Error when execution failed.
 * @param string     $ability_name Name of the ability.
 * @param mixed      $input        Normalized input.
 * @param WP_Ability $ability      Ability instance.
 */
apply_filters(
    'wp_ability_execute_result',
    $result,
    $ability_name,
    $input,
    $ability
);

Possible uses include:

  • Formatting a response.
  • Removing internal metadata.
  • Applying content-safety filtering.
  • Enriching a result.
  • Converting a successful result into an error.
  • Recovering from an execution error.

Removing internal response data

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 filtered result is subsequently validated against the ability’s output_schema.

Recovering from selected execution failures

The filter receives WP_Error values produced by the registered callback, so plugins may implement narrowly scoped recovery behaviour:

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
);

Any recovered value must still conform to the registered output_schema.

New WP_Filter_Sentinel class

WordPress 7.1 also introduces WP_Filter_Sentinel, a reusable marker class loaded alongside WP_Hook.

CoreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. uses a unique sentinel instance as the default value for wp_pre_execute_ability. Comparing object identity allows Core to distinguish an unchanged default from every possible user-supplied value, including null, false, arrays and arbitrary objects.

Developers using wp_pre_execute_ability do not need to instantiate this class. To continue normal execution, callbacks should simply return the received $pre value unchanged.

Backward compatibility

These changes are additive:

  • Existing abilities require no changes.
  • Existing wp_before_execute_ability and wp_after_execute_ability callbacks continue to work.
  • Ability callbacks and permission callbacks retain their existing behaviour when none of the new filters is used.
  • Input and output schemas remain the final validation boundaries for normal execution.

Plugins that already provide their own ability-execution hooksHooks In WordPress theme and development, hooks are functions that can be applied to an action or a Filter in WordPress. Actions are functions performed when a certain event occurs in WordPress. Filters allow you to modify certain functions. Arguments used to hook both filters and actions look the same. may wish to evaluate whether some functionality can now use these Core filters. Protocol- or domain-specific hooks can still be layered on top when they need more specialised context.

Summary

WordPress 7.1 adds the following Abilities API filters:

FilterPurpose
wp_pre_execute_abilityShort-circuit the complete execution pipeline
wp_ability_normalize_inputTransform normalized input before validation
wp_ability_permission_resultModify or override permission results
wp_ability_execute_resultTransform or recover results before output validation

These filters make the Abilities API more extensibleExtensible This is the ability to add additional functionality to the code. Plugins extend the WordPress core software. for AI integrations, automation systems, protocol adapters, authorisation layers and other tools that mediate ability execution.

For additional context, see Trac ticket #64989 and changeset [62397].

Props to @benjamin_zekavica and @audrasjb for peer review.

#abilities-api, #7-1, #dev-notes, #dev-notes-7-1

Abilities API in WordPress 6.9

WordPress 6.9 introduces the Abilities APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways., a new foundational system that enables plugins, themes, and WordPress coreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. to register and expose their capabilitiescapability A capability is permission to perform one or more types of task. Checking if a user has a capability is performed by the current_user_can function. Each user of a WordPress site might have some permissions but not others, depending on their role. For example, users who have the Author role usually have permission to edit their own posts (the “edit_posts” capability), but not permission to edit other users’ posts (the “edit_others_posts” capability). in a standardized, machine-readable format. This API creates a unified registry of functionality that can be discovered, validated, and executed consistently across different contexts, including PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 7.4 or higher, REST APIREST API The REST API is an acronym for the RESTful Application Program Interface (API) that uses HTTP requests to GET, PUT, POST and DELETE data. It is how the front end of an application (think “phone app” or “website”) can communicate with the data store (think “database” or “file system”) https://developer.wordpress.org/rest-api/ endpoints, and future AI-powered integrations.

The Abilities API is part of the broader AI Building Blocks for WordPress initiative, providing the groundwork for AI agents, automation tools, and developers to understand and interact with WordPress functionality in a predictable manner.

What is the Abilities API?

An ability is a self-contained unit of functionality with defined inputs, outputs, permissions, and execution logic. By registering abilities through the Abilities API, developers can:

  • Create discoverable functionality with standardized interfaces
  • Define permission checks and execution callbacks
  • Organize abilities into logical categories
  • Validate inputs and outputs
  • Automatically expose abilities through REST API endpoints

Rather than burying functionality in isolated functions or custom AJAX handlers, abilities are registered in a central registry that makes them accessible through multiple interfaces.

Core Components

The Abilities API introduces three main components to WordPress 6.9:

1. PHP API

A set of functions for registering, managing, and executing abilities:

Ability Management:

  • wp_register_ability() – Register a new ability
  • wp_unregister_ability() – Unregister an ability
  • wp_has_ability() – Check if an ability is registered
  • wp_get_ability() – Retrieve a registered ability
  • wp_get_abilities() – Retrieve all registered abilities

Ability CategoryCategory The 'category' taxonomy lets you group posts / content together that share a common bond. Categories are pre-defined and broad ranging. Management:

  • wp_register_ability_category() – Register an ability category
  • wp_unregister_ability_category() – Unregister an ability category
  • wp_has_ability_category() – Check if an ability category is registered
  • wp_get_ability_category() – Retrieve a registered ability category
  • wp_get_ability_categories() – Retrieve all registered ability categories

2. REST API Endpoints

When enabled, the Abilities API can automatically expose registered abilities through REST API endpoints under the wp-abilities/v1 namespace:

  • GET /wp-abilities/v1/categories – List all ability categories
  • GET /wp-abilities/v1/categories/{slug} – Get a single ability category
  • GET /wp-abilities/v1/abilities – List all abilities
  • GET /wp-abilities/v1/abilities/{name} – Get a single ability
  • GET|POST|DELETE /wp-abilities/v1/abilities/{name}/run – Execute an ability

3. HooksHooks In WordPress theme and development, hooks are functions that can be applied to an action or a Filter in WordPress. Actions are functions performed when a certain event occurs in WordPress. Filters allow you to modify certain functions. Arguments used to hook both filters and actions look the same.

New action hooks for integrating with the Abilities API:

Actions:

  • wp_abilities_api_categories_init – Fired when the ability categories registry is initialized (register categories here)
  • wp_abilities_api_init – Fired when the abilities registry is initialized (register abilities here)
  • wp_before_execute_ability – Fired before an ability executes
  • wp_after_execute_ability – Fired after an ability finishes executing

Filters:

  • wp_register_ability_category_args – Filters ability category arguments before registration
  • wp_register_ability_args – Filters ability arguments before registration

Registering Abilities

Abilities must be registered on the wp_abilities_api_init action hook. Attempting to register abilities outside of this hook will trigger a _doing_it_wrong() notice, and the Ability registration will fail.

Basic Example

Here’s a complete example of registering an ability category and an ability:

<?php
add_action( 'wp_abilities_api_categories_init', 'my_plugin_register_ability_categories' );
/**
 * Register ability categories.
 */
function my_plugin_register_ability_categories() {
    wp_register_ability_category(
        'content-management',
        array(
            'label'       => __( 'Content Management', 'my-plugin' ),
            'description' => __( 'Abilities for managing and organizing content.', 'my-plugin' ),
        )
    );
}

add_action( 'wp_abilities_api_init', 'my_plugin_register_abilities' );
/**
 * Register abilities.
 */
function my_plugin_register_abilities() {
    wp_register_ability(
        'my-plugin/get-post-count',
        array(
            'label'              => __( 'Get Post Count', 'my-plugin' ),
            'description'        => __( 'Retrieves the total number of published posts.', 'my-plugin' ),
            'category'           => 'content-management',
            'input_schema'       => array(
                'type'       => 'string',
                'description' => __( 'The post type to count.', 'my-plugin' ),
                'default'     => 'post',
            ),
            'output_schema'      => array(
                'type'       => 'integer',
                'description' => __( 'The number of published posts.', 'my-plugin' ),
            ),
            'execute_callback'   => 'my_plugin_get_post_count',
            'permission_callback' => function() {
                return current_user_can( 'read' );
            },
        )
    );
}

/**
 * Execute callback for get-post-count ability.
 */
function my_plugin_get_post_count( $input ) {
    $post_type = $input ?? 'post';

    $count = wp_count_posts( $post_type );

    return (int) $count->publish;
}

More Complex Example

Here’s an example with more advanced input and output schemas, input validation, and error handling:

<?php
add_action( 'wp_abilities_api_init', 'my_plugin_register_text_analysis_ability' );
/**
 * Register a text analysis ability.
 */
function my_plugin_register_text_analysis_ability() {
    wp_register_ability(
        'my-plugin/analyze-text',
        array(
            'label'              => __( 'Analyze Text', 'my-plugin' ),
            'description'        => __( 'Performs sentiment analysis on provided text.', 'my-plugin' ),
            'category'           => 'text-processing',
            'input_schema'       => array(
                'type'       => 'object',
                'properties' => array(
                    'text' => array(
                        'type'        => 'string',
                        'description' => __( 'The text to analyze.', 'my-plugin' ),
                        'minLength'   => 1,
                        'maxLength'   => 5000,
                    ),
                    'options' => array(
                        'type'       => 'object',
                        'properties' => array(
                            'include_keywords' => array(
                                'type'        => 'boolean',
                                'description' => __( 'Whether to extract keywords.', 'my-plugin' ),
                                'default'     => false,
                            ),
                        ),
                    ),
                ),
                'required' => array( 'text' ),
            ),
            'output_schema'      => array(
                'type'       => 'object',
                'properties' => array(
                    'sentiment' => array(
                        'type'        => 'string',
                        'enum'        => array( 'positive', 'neutral', 'negative' ),
                        'description' => __( 'The detected sentiment.', 'my-plugin' ),
                    ),
                    'confidence' => array(
                        'type'        => 'number',
                        'minimum'     => 0,
                        'maximum'     => 1,
                        'description' => __( 'Confidence score for the sentiment.', 'my-plugin' ),
                    ),
                    'keywords' => array(
                        'type'        => 'array',
                        'items'       => array(
                            'type' => 'string',
                        ),
                        'description' => __( 'Extracted keywords (if requested).', 'my-plugin' ),
                    ),
                ),
            ),
            'execute_callback'   => 'my_plugin_analyze_text',
            'permission_callback' => function() {
                return current_user_can( 'edit_posts' );
            },
        )
    );
}

/**
 * Execute callback for analyze-text ability.
 * 
 * @param $input
 * @return array
 */
function my_plugin_analyze_text( $input ) {
    $text = $input['text'];
    $include_keywords = $input['options']['include_keywords'] ?? false;

    // Perform analysis (simplified example)
    $sentiment = 'neutral';
    $confidence = 0.75;

    $result = array(
        'sentiment'  => $sentiment,
        'confidence' => $confidence,
    );

    if ( $include_keywords ) {
        $result['keywords'] = array( 'example', 'keyword' );
    }

    return $result;
}

Ability Naming Conventions

Ability names should follow these practices:

  • Use namespaced names to prevent conflicts (e.g., my-plugin/my-ability)
  • Use only lowercase alphanumeric characters, dashes, and forward slashes
  • Use descriptive, action-oriented names (e.g., process-payment, generate-report)
  • The format should be namespace/ability-name

Executing Abilities

To execute an Ability, first you need to fetch it, and then execute it. This is typically performing during the init action, or any action after it.

add_action( 'init', 'my_plugin_execute_ability' );
/**
 * Function which fetches and executes an ability.
 * 
 * @return void
 */
function my_plugin_execute_ability() {
    $get_post_count_ability = wp_get_ability( 'my-plugin/get-post-count' );
    $result = $get_post_count_ability->execute();
    // do something with $result
}

Categories

Abilities must be assigned to a category. Categories provide better discoverability and help organize related abilities. Categories must be registered before the abilities that reference them using the wp_abilities_api_categories_init hook.

JSONJSON JSON, or JavaScript Object Notation, is a minimal, readable format for structuring data. It is used primarily to transmit data between a server and web application, as an alternative to XML. Schema Validation

The Abilities API uses JSON Schema for input and output validation. WordPress implements a validator based on a subset of JSON Schema Version 4. The schemas serve two purposes:

  1. Automatic validation of data passed to and returned from abilities
  2. Self-documenting API contracts for developers

Defining schemas is mandatory when there is a value to pass or return.

Using REST API Endpoints

Developers can also enable Abilities to support the default REST API endpoints. This is possible by setting the meta.show_in_rest argument to true when registering an ability.

       wp_register_ability(
        'my-plugin/get-post-count',
        array(
            'label'              => __( 'Get Post Count', 'my-plugin' ),
            'description'        => __( 'Retrieves the total number of published posts.', 'my-plugin' ),
            'category'           => 'content-management',
            'input_schema'       => array(
                'type'       => 'string',
                'description' => __( 'The post type to count.', 'my-plugin' ),
                'default'     => 'post',
            ),
            'output_schema'      => array(
                'type'       => 'integer',
                'description' => __( 'The number of published posts.', 'my-plugin' ),
            ),
            'execute_callback'   => 'my_plugin_get_post_count',
            'permission_callback' => function() {
                return current_user_can( 'read' );
            },
            'meta'               => array(
                'show_in_rest' => true,
            )
        )
    );

Access to all Abilities REST API endpoints requires an authenticated user. The Abilities API supports all WordPress REST API authentication methods:

  • Cookie authentication (same-origin requests)
  • Application passwords (recommended for external access)
  • Custom authentication plugins

Once enabled, it’s possible to list, fetch, and execute Abilities via the REST API endpoints:

List All Abilities:

curl -u 'USERNAME:APPLICATION_PASSWORD' \
  https://example.com/wp-json/wp-abilities/v1/abilities

Get a Single Ability:

curl -u 'USERNAME:APPLICATION_PASSWORD' \
https://example.com/wp-json/wp-abilities/v1/abilities/my-plugin/get-post-count

Execute an Ability:

curl -u 'USERNAME:APPLICATION_PASSWORD' \
  -X POST https://example.com/wp-json/wp-abilities/v1/abilities/my-plugin/get-post-count/run \
  -H "Content-Type: application/json" \
  -d '{"input": {"post_type": "page"}}'

The API automatically validates the input against the ability’s input schema, checks permissions via the ability’s permission callback, executes the ability, validates the output against the ability’s output schema, and returns the result as JSON.

Checking and Retrieving Abilities

You can check if an ability exists and retrieve it programmatically:

<?php
// Check if an ability is registered
if ( wp_has_ability( 'my-plugin/get-post-count' ) ) {
    // Get the ability object
    $ability = wp_get_ability( 'my-plugin/get-post-count' );

    // Access ability properties
    echo $ability->get_label();
    echo $ability->get_description();
}

// Get all registered abilities
$all_abilities = wp_get_abilities();

foreach ( $all_abilities as $ability ) {
    echo $ability->get_name();
}

Error Handling

Abilities should handle errors gracefully by returning WP_Error objects:

<?php
function my_plugin_delete_post( $input ) {
    $post_id = $input['post_id'];

    if ( ! get_post( $post_id ) ) {
        return new WP_Error(
            'post_not_found',
            __( 'The specified post does not exist.', 'my-plugin' ),
        );
    }

    $result = wp_delete_post( $post_id, true );

    if ( ! $result ) {
        return new WP_Error(
            'deletion_failed',
            __( 'Failed to delete the post.', 'my-plugin' ),
        );
    }

    return array(
        'success' => true,
        'post_id' => $post_id,
    );
}

Backward Compatibility

The Abilities API is a new feature in WordPress 6.9 and does not affect existing WordPress functionality. Plugins and themes can adopt the API incrementally without breaking existing code.

For developers who want to support both WordPress 6.9+ and earlier versions, check if the API functions exist before using them:

<?php
if ( function_exists( 'wp_register_ability' ) ) {
    add_action( 'wp_abilities_api_init', 'my_plugin_register_abilities' );
}

Or

if ( class_exists( 'WP_Ability' ) ) {
 add_action( 'wp_abilities_api_init', 'my_plugin_register_abilities' );
}

Further Resources

Props to @gziolo for pre-publish review.

#abilities-api, #6-9, #dev-notes, #dev-notes-6-9