WordPress plugins have never lacked ways to perform work. A plugin can expose a PHP function, register a REST route, add a WP-CLI command, respond to an admin action or wire a custom AJAX handler into the dashboard. The awkward part begins when the same operation must be available in several places.
Take a plugin that can generate a report. Its dashboard button needs one implementation. A remote integration needs a REST route. A command-line workflow needs a WP-CLI command. An AI assistant needs a tool definition with a machine-readable description of its arguments. Unless the plugin is designed carefully, each new interface grows its own validation, permission logic and error handling.
The WordPress Abilities API gives that operation a common contract. A plugin registers a named ability, describes the input and output with JSON Schema, supplies an execution callback and defines who may run it. Other code can then discover the ability and invoke it through Core instead of learning the plugin’s private internals.
The API first shipped in WordPress 6.9. WordPress 7.1, released on August 19, 2026, is the version that makes it substantially more useful as a public integration layer. It adds filtered discovery, a unified public-exposure flag, client-ready schema preparation, richer validation and hooks across the execution lifecycle. Those changes are summarized in the official WordPress 7.1 Field Guide, while the Abilities API handbook covers the underlying API introduced in 6.9.
This guide builds a small but complete ability and follows it from registration to PHP, REST and MCP. The example is deliberately ordinary: given a post ID, return a compact brief for that post. That keeps the focus on the contract, where most of the important decisions live.
What an ability actually represents
An ability is a distinct thing a WordPress site knows how to do. Good candidates tend to sound like deliberate actions: get a product’s stock status, create a support ticket, calculate shipping for a cart, regenerate an image variant or publish a scheduled report.
That is narrower than a typical REST resource. A REST controller for posts may support listing, reading, creating, updating and deleting. An ability should usually express one outcome. The narrow scope makes its description clearer, its permissions easier to review and its schema more useful to an automated client.
An ability is also transport-neutral. Calling wp_register_ability() does not open an anonymous endpoint and does not turn WordPress into an MCP server. It registers a callable contract in WordPress. Core’s Abilities REST API can expose that contract over HTTP. The official MCP Adapter can translate opted-in abilities for MCP clients. Internal PHP can invoke the same ability without either transport.
This separation is the part worth holding onto. Your business logic should not care whether the caller was a dashboard screen, a scheduled job, an authenticated REST client or an AI agent. The caller changes; the contract and authorization rules do not.
What WordPress 7.1 changes
The base registration API remains compatible with 6.9. WordPress 7.1 improves what happens around registration and execution.
| WordPress 7.1 change | Why it matters in a plugin |
|---|---|
Filtered wp_get_abilities() queries | Integrations can request one namespace, category or metadata profile without loading the registry and inventing their own filtering rules. |
meta.public | A plugin can state once that an ability is intended for external clients, while retaining channel-specific overrides. |
| Client schema preparation | Internal WordPress schemas can be converted to portable Draft 4 JSON Schema at the boundary where they leave WordPress. |
| Execution lifecycle filters | Plugins can short-circuit a call, normalize input, add authorization policy or transform a result at defined stages. |
| Custom validation filters and invocation telemetry | Domain rules can supplement JSON Schema, and every invocation can be observed even when it fails early. |
| Typed REST input | Query-string values for GET and DELETE ability runs can arrive as the integer, boolean or array types declared by the schema. |
None of these features weakens the central rule: discovery is not authorization. An ability can be visible to a client and still be impossible for that client’s current WordPress user to execute.
Design the contract before writing the callback
It is tempting to begin with the PHP function you already have and wrap it. Spend a few minutes on the boundary first.
Our example ability will be named wpbay-content/get-post-brief. The namespace belongs to the plugin. The second half says exactly what is returned. It will accept a positive integer post_id and an optional boolean include_excerpt. It will return a stable object containing the ID, title, post type, status, permalink and excerpt.
The ability only reads data, so its readonly annotation is true. Repeating the same call does not create another side effect, so idempotent is also true. It is suitable for authenticated external clients, so public is true. Its permission callback checks read_post against the requested post rather than granting a broad capability such as edit_posts.
Those details are more than documentation. Core validates the input before checking permission, validates the callback’s result before returning it, and uses the annotations to determine the appropriate REST method. External tooling can use the same metadata to decide which operations it is willing to offer.
Register a complete ability
The following can live in the main file of a small plugin. In a larger plugin, the category registration, schema and callbacks would normally sit in separate classes, but the underlying sequence is the same.
<?php
/**
* Plugin Name: WPBay Content Abilities
* Description: A practical WordPress 7.1 Abilities API example.
* Version: 1.0.0
* Requires at least: 7.1
* Requires PHP: 7.4
* Text Domain: wpbay-content-abilities
*/
defined( 'ABSPATH' ) || exit;
add_action(
'wp_abilities_api_categories_init',
'wpbay_content_register_ability_category'
);
function wpbay_content_register_ability_category(): void {
wp_register_ability_category(
'content-inspection',
array(
'label' => __( 'Content Inspection', 'wpbay-content-abilities' ),
'description' => __( 'Read-only tools that inspect WordPress content.', 'wpbay-content-abilities' ),
)
);
}
add_action( 'wp_abilities_api_init', 'wpbay_content_register_abilities' );
function wpbay_content_register_abilities(): void {
wp_register_ability(
'wpbay-content/get-post-brief',
array(
'label' => __( 'Get Post Brief', 'wpbay-content-abilities' ),
'description' => __( 'Returns a concise brief for a post the current user is allowed to read.', 'wpbay-content-abilities' ),
'category' => 'content-inspection',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'post_id' => array(
'type' => 'integer',
'minimum' => 1,
'description' => __( 'The ID of the post to inspect.', 'wpbay-content-abilities' ),
),
'include_excerpt' => array(
'type' => 'boolean',
'default' => true,
'description' => __( 'Whether to include a short plain-text excerpt.', 'wpbay-content-abilities' ),
),
),
'required' => array( 'post_id' ),
'additionalProperties' => false,
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'id' => array( 'type' => 'integer' ),
'title' => array( 'type' => 'string' ),
'post_type' => array( 'type' => 'string' ),
'status' => array( 'type' => 'string' ),
'url' => array(
'type' => 'string',
'format' => 'uri',
),
'excerpt' => array( 'type' => 'string' ),
),
'required' => array(
'id',
'title',
'post_type',
'status',
'url',
'excerpt',
),
'additionalProperties' => false,
),
'permission_callback' => 'wpbay_content_can_get_post_brief',
'execute_callback' => 'wpbay_content_get_post_brief',
'meta' => array(
'annotations' => array(
'readonly' => true,
'destructive' => false,
'idempotent' => true,
),
'public' => true,
),
)
);
}
function wpbay_content_can_get_post_brief( array $input ) {
return current_user_can( 'read_post', (int) $input['post_id'] );
}
function wpbay_content_get_post_brief( array $input ) {
$post = get_post( (int) $input['post_id'] );
if ( ! $post instanceof WP_Post ) {
return new WP_Error(
'wpbay_post_not_found',
__( 'The requested post could not be found.', 'wpbay-content-abilities' ),
array( 'status' => 404 )
);
}
$url = get_permalink( $post );
if ( false === $url ) {
return new WP_Error(
'wpbay_post_has_no_url',
__( 'WordPress could not create a URL for this post.', 'wpbay-content-abilities' )
);
}
$include_excerpt = $input['include_excerpt'] ?? true;
$excerpt = '';
if ( $include_excerpt ) {
$source = has_excerpt( $post )
? $post->post_excerpt
: strip_shortcodes( $post->post_content );
$excerpt = wp_trim_words( wp_strip_all_tags( $source ), 40, '…' );
}
return array(
'id' => $post->ID,
'title' => get_the_title( $post ),
'post_type' => $post->post_type,
'status' => $post->post_status,
'url' => $url,
'excerpt' => $excerpt,
);
}Categories and abilities use different initialization hooks. The category must exist by the time the ability is registered. A category slug accepts lowercase letters, numbers and hyphens. An ability name uses the namespace/ability-name pattern and likewise avoids uppercase letters and underscores. Core’s PHP reference documents the registration rules and callback contract in detail.
The schema rejects extra properties. That is a useful default for an operation likely to be called by generated code or an agent: a misspelled field should fail visibly instead of being ignored. The optional flag has a schema default, so the execute callback receives include_excerpt as true when the caller omits it.
The permission check is deliberately object-aware. A subscriber may read a published post. An editor may also read a private post. Passing the ID into current_user_can( 'read_post', $post_id ) lets WordPress map the meta capability for the requested object. A blanket __return_true would make private content retrievable by every authenticated account once the ability is exposed.
The callback returns WP_Error for an operational failure. Throwing a generic exception or returning a differently shaped array defeats the contract. A caller using PHP can inspect the error. The REST controller can turn its status data into an HTTP response. A protocol adapter can translate it for its own client.
Execute the ability from PHP
Internal code should retrieve the registered object and call its execute() method. Do not call the execute callback directly. Going through WP_Ability keeps normalization, schema validation, permissions and the lifecycle hooks in the path.
$ability = wp_get_ability( 'wpbay-content/get-post-brief' );
if ( ! $ability ) {
return new WP_Error(
'wpbay_ability_unavailable',
__( 'The post brief ability is unavailable.', 'wpbay-content-abilities' )
);
}
$result = $ability->execute(
array(
'post_id' => 42,
'include_excerpt' => false,
)
);
if ( is_wp_error( $result ) ) {
error_log( $result->get_error_message() );
return $result;
}
// $result now conforms to the registered output schema.There is a separate check_permissions() method, but most callers should not use it as a preliminary gate and then assume execution is safe. State can change between the two calls, and execute() performs its own check anyway. Call execute() and handle the result. Use check_permissions() independently only when a user interface needs to decide whether to display an action, with the understanding that the later execution can still be denied.
Discover abilities without scanning the whole registry
Before WordPress 7.1, wp_get_abilities() returned every registered ability and left consumers to filter the array. That produced slightly different discovery rules in every adapter. In 7.1, the function accepts an argument array for category, namespace and nested metadata matching.
$abilities = wp_get_abilities(
array(
'namespace' => 'wpbay-content',
'meta' => array(
'public' => true,
'annotations' => array(
'readonly' => true,
),
),
)
);The conditions use AND logic, and metadata comparisons are strict. Boolean true is different from integer 1. Namespace matching respects the slash delimiter, so wpbay-content will not accidentally include a namespace named wpbay-content-pro.
For a one-off rule, the same call can receive an item_include_callback or a result_callback. Site-wide filtering is possible through wp_get_abilities_item_include and wp_get_abilities_result, although global filters deserve restraint because they affect other plugins as well as your own. The 7.1 discovery dev note documents the complete order of the filtering pipeline.
One subtle point matters for REST. The collection endpoint applies show_in_rest = true internally in addition to any query the client sends. A metadata filter cannot be used to reveal an ability that the plugin kept out of REST.
public means exposable, not unauthenticated
WordPress 7.1 resolves a boolean meta.public value for every ability. It defaults to false. Setting it to true tells integrations that the ability is generally intended for use outside the plugin’s own PHP code.
For the built-in REST channel, public: true makes show_in_rest default to true. An explicit channel setting takes precedence:
'meta' => array(
'public' => true,
'show_in_rest' => false,
),That configuration says the ability is suitable for external integrations in general but should remain hidden from Core’s REST endpoints. The inverse is also valid: public can be false while show_in_rest is explicitly true for a REST-only use case. The exact precedence is documented in the Core note on the unified public exposure flag.
The word “public” can be misleading if read as an access-control term. It does not bypass REST authentication. It does not replace the ability’s permission_callback. It does not make private post data public. Think of it as a distribution preference: this contract may be advertised through compatible channels, subject to the identity and capabilities of the caller.
This is why a good permission callback is non-negotiable. It should be based on WordPress capabilities and, where possible, the exact object or scope in the input. A destructive ability should usually require a stronger capability than the screen from which it happens to be called. Never authorize an AI integration by checking a request header invented by the plugin while leaving the underlying WordPress user unrestricted.
Call the ability through the REST API
Core exposes abilities under /wp-json/wp-abilities/v1. The collection route lists exposed abilities, the item route returns one ability’s definition, and the /run route invokes it. All of these routes require an authenticated WordPress user. For an external service, an Application Password is normally the simplest Core-supported credential. Same-origin JavaScript can use the logged-in cookie and a REST nonce.
Because the example is annotated as read-only, its run route uses GET. WordPress 7.1 coerces query-string values using the input schema, so 42 arrives as an integer and true arrives as a boolean.
curl --user 'integration-user:xxxx xxxx xxxx xxxx xxxx xxxx' \
--get 'https://example.com/wp-json/wp-abilities/v1/wpbay-content/get-post-brief/run' \
--data-urlencode 'input[post_id]=42' \
--data-urlencode 'input[include_excerpt]=true'For a write operation, the usual method is POST and the JSON body wraps arguments in an input object. An ability annotated as destructive uses DELETE. Clients should discover the ability metadata rather than hard-code a method based only on its name. The official Abilities REST endpoint reference covers the route shapes, supported authentication and standard error codes.
Do not put an Application Password in a public JavaScript bundle. Store it in the external service that makes the server-to-server request, and give its WordPress account only the capabilities the integration needs. If a reverse proxy or hosting layer strips the Authorization header, fix that server configuration instead of moving credentials into query parameters.
The collection endpoint supports the same declarative discovery introduced for PHP. An authenticated client can request ?namespace=wpbay-content or filter standard annotations with a query such as ?meta[annotations][readonly]=true. Core knows those annotation values are booleans and coerces them accordingly. Custom nested metadata needs a schema added through rest_abilities_collection_params if non-string query values must survive strict matching.
How MCP and AI clients fit in
The Abilities API is often discussed as part of WordPress’s AI work, but an ability does not need to call a model and it is not limited to AI. Its value is that a machine can understand the operation without reading the plugin source.
The official WordPress MCP Adapter bridges registered abilities to the Model Context Protocol. In its current exposure model, abilities remain private by default. The adapter accepts either the general meta.public flag or the channel-specific meta.mcp.public flag, then makes opted-in abilities available through its discovery, information and execution tools. It supplies HTTP and STDIO transports and still runs each ability’s WordPress permission check.
Installing that adapter is a separate deployment decision. meta.public expresses your plugin’s intent; it does not install a server, choose transport authentication or connect Claude, ChatGPT or another client. This boundary lets a plugin be integration-ready without forcing an MCP dependency onto every site.
Descriptions become unusually important once an agent can choose among abilities. “Gets post data” is vague. “Returns a concise brief for a post the current user is allowed to read” describes the result and hints at the authorization boundary. Input property descriptions should explain units, formats and defaults. Output descriptions should distinguish IDs, display labels and machine values. Clear descriptions improve ordinary API documentation as much as they improve tool selection by a model.
WordPress’s AI Client can also represent callable functions for models. In 7.1, Core prepares ability input schemas when they are converted into AI Client function declarations. The same contract can therefore inform model tooling without the plugin maintaining another hand-written parameter description.
Keep one canonical schema
WordPress uses a JSON Schema vocabulary for validation, but internal WordPress schemas sometimes contain conveniences that should not leave the server. A property may use a local required flag. REST argument schemas may contain PHP callbacks. An empty PHP array intended as an object would be encoded as a JSON array if sent unchanged.
WordPress 7.1 adds wp_prepare_json_schema_for_client() for that output boundary. The function can move property-level required markers into the containing object’s required array, remove server-only callback keys recursively and preserve empty object defaults correctly. It returns a prepared copy; it does not change the schema used by the ability at runtime.
Most ability authors never need to call it. Core already prepares schemas returned by the Abilities REST API and those converted to AI Client function declarations. A custom endpoint or adapter should use it instead of maintaining a second schema by hand:
$ability = wp_get_ability( 'wpbay-content/get-post-brief' );
$schema = $ability ? $ability->get_input_schema() : null;
if ( is_array( $schema ) ) {
$client_schema = wp_prepare_json_schema_for_client(
$schema,
'draft-04'
);
}The default draft-04 profile is the broad choice for MCP and AI consumers. The rest-api profile keeps to the narrower vocabulary supported by WordPress REST. Both produce Draft 4 output. Neither guarantees compatibility with every model provider, because providers may accept smaller subsets of JSON Schema. If a provider needs further restrictions, compile the prepared schema for that provider at the final adapter layer rather than weakening the canonical ability schema.
There is another trap here. Adding validate_callback or sanitize_callback keys inside an ability schema does not make the Abilities runtime execute those callbacks. They are removed from client-facing output, and Core’s ability validation does not treat them as runtime hooks. WordPress 7.1 supplies dedicated validation and normalization filters for that job. The client schema preparation dev note explains the transformations and the distinction between server and client schemas.
Work with the execution lifecycle, not around it
An ability execution in 7.1 has a defined order. Core first fires wp_ability_invoked. It then offers the wp_pre_execute_ability short circuit, normalizes input and applies wp_ability_normalize_input, validates the input, checks the registered permission callback and applies wp_ability_permission_result. Only then does it run the execute callback. The result passes through wp_ability_execute_result, output validation and the final observation action.
That order determines where an extension belongs. A cache lookup or maintenance switch can short-circuit the whole call. A string-to-ID conversion belongs in normalization. An organization-wide policy can add to the permission result. Redaction of an internal field belongs in result transformation, before the output is validated.
For example, a site may temporarily suspend an expensive synchronization ability:
add_filter(
'wp_pre_execute_ability',
function ( $pre, $ability_name, $input, $ability ) {
if ( 'wpbay-content/sync-library' !== $ability_name ) {
return $pre;
}
if ( ! get_option( 'wpbay_content_pause_sync', false ) ) {
return $pre;
}
return new WP_Error(
'wpbay_sync_paused',
__( 'Library synchronization is temporarily paused.', 'wpbay-content-abilities' ),
array( 'status' => 503 )
);
},
10,
4
);The $pre value is a unique sentinel object supplied by Core. Returning the same value continues normal execution. Returning anything else, including null or false, is a real override and bypasses validation and permission checks along with the callback. A short circuit should therefore be narrow and should not return protected data merely because it found a cache entry. If a cache varies by user permissions, the filter must enforce that boundary itself or be placed later in an architecture that does.
The permission-result filter is similarly powerful. It runs after the ability’s own permission callback. Returning true can overturn an original denial, so an additional policy should preserve both false and WP_Error before checking its extra rules. The official execution lifecycle dev note includes the signatures and precise order of all four filters.
Add domain validation without leaking information
JSON Schema handles shape well: types, required properties, allowed values, lengths and numeric boundaries. Business rules can depend on relationships the schema does not express. WordPress 7.1 adds wp_ability_validate_input and wp_ability_validate_output for those cases.
Each filter receives the validation result Core already produced, the value and the ability name. Preserve an existing WP_Error; otherwise a custom validator can accidentally replace a useful schema error. Return true for valid data or a specific WP_Error for invalid data. Returning false works, but Core has to turn it into a generic error that is harder for clients to diagnose.
Be thoughtful about checks that touch protected records. Input validation runs before the permission callback. If it reports “Order 983 does not exist” to a user who is not allowed to inspect orders, it has created an enumeration signal. Cross-field rules are safe candidates for input validation. Checks involving confidential objects often belong after authorization, inside the execute callback, with deliberately restrained errors.
Output validation is especially valuable during development. If the callback begins returning an integer where its schema promises a string, the ability fails at its own boundary instead of sending an unstable response to every consumer. Treat that failure as a plugin bug, not something an adapter should silently coerce away.
Observe calls without logging secrets
The wp_ability_invoked action fires at the very start of every execute() call. It runs for successful calls, malformed input, denied users, cached results and calls stopped by wp_pre_execute_ability. That makes it a reliable place to count invocation attempts.
It receives raw input. Raw ability input can contain email addresses, unpublished content, API tokens or customer data. Logging the whole value to debug.log is a poor default. Record the ability name, authenticated user ID, time, request correlation ID and eventual status in a proper application log. Add selected input fields only after an explicit data review.
The older wp_before_execute_ability and wp_after_execute_ability actions still exist. In 7.1 they also receive the corresponding WP_Ability object as a final argument. Existing callbacks remain compatible; increase the accepted argument count only when you need the object. Core’s 7.1 Abilities improvements note covers the new validation filters, invocation action and REST input coercion.
Support WordPress 6.9 and 7.1 without two implementations
If a plugin requires WordPress 7.1, declare that in its header and use the current API directly. If it still supports 6.9 or 7.0, the registered ability can remain the same. The 7.1 improvements are additive.
Guard only the features that arrived in 7.1. Calling wp_get_abilities() with no arguments works across these versions, while passing the new filter arguments requires 7.1. meta.public is a 7.1 convention, so a cross-version plugin that needs REST exposure should retain an explicit show_in_rest => true until its minimum version moves forward. Lifecycle filters can be attached on older versions without causing an error, but nothing will apply them; code that depends on the effect should check the WordPress version or function availability.
For sites older than 6.9, there is no Core Abilities API. A runtime check such as class_exists( 'WP_Ability' ) can prevent a fatal error, but it does not create a useful fallback. If the ability is central to the plugin, show a clear admin requirement notice and keep the plugin’s existing internal service layer available to legacy UI code. Do not bundle a private copy of Core’s API under the same global names.
Test the contract, not only the callback
Calling wpbay_content_get_post_brief() directly proves very little. It skips the behavior that makes an ability valuable. The main integration test should retrieve the registered WP_Ability and invoke execute() as a real user.
A useful test creates a published post and a private post, switches among subscriber, author and editor accounts, then checks the exact returned shape or error. A second test passes a string ID, a missing ID, zero and an unexpected property to confirm schema behavior. Another deliberately changes a callback result in a fixture so output validation is proven rather than assumed.
The REST layer deserves its own coverage because authentication, input coercion and HTTP method selection do not run in a PHP-only test. Exercise the collection endpoint, the item definition and /run with an Application Password belonging to a low-privilege test user. Verify that a private ability is absent from discovery, that an exposed ability is still denied when the user lacks read_post, and that disabling show_in_rest wins over public.
If the plugin advertises MCP compatibility, include the adapter in an integration environment. Test discovery and execution through the adapter rather than stopping after meta.public appears in the registered data. The MCP transport has its own authentication and configuration boundary, and a green PHP test cannot prove it is correct.
Finally, run the same contract tests against the oldest WordPress version the plugin claims to support. If that version is 6.9, the suite should expect explicit show_in_rest; if it is 7.1, test the resolved public behavior and filtered discovery.
Failures that usually point to the registration, not the transport
When wp_get_ability() returns null, check the hook and category first. The ability must be registered during wp_abilities_api_init, and its category must have been registered during wp_abilities_api_categories_init. Invalid slugs and duplicate names also cause registration to fail.
When an ability works in PHP but is missing from REST, inspect its resolved metadata. On 7.1, public normally enables REST, but an explicit false show_in_rest wins. The REST collection also requires authentication, so testing the URL in a private browser window is not a valid discovery check.
When execution returns ability_invalid_input, compare the actual PHP types to the schema. Internal callers do not receive REST’s query-string coercion. An internal caller passing '42' to an integer field has supplied a string unless a normalization filter changes it. Defaults only apply when the schema declares them.
An ability_invalid_output error means the callback broke its promise. Look for missing required keys, values with unexpected types and extra properties when additionalProperties is false. Fix the callback or revise the schema deliberately. Do not disable output validation to conceal drift.
A REST 401 points to authentication before ability permissions are considered. A 403 or permission error means WordPress knows who the user is but the ability rejected that user or object. Keeping those cases distinct saves a great deal of unproductive debugging.
When an MCP client cannot see an ability that REST can see, confirm the MCP Adapter is installed, its server is reachable and either meta.public or meta.mcp.public is true. REST visibility alone is not proof that an MCP server is active.
When an ability is the right abstraction
Register an ability when an operation should be discoverable, independently executable and described by a stable input/output contract. It is a strong fit for automation steps, agent tools, reusable dashboard actions and cross-plugin integrations.
Keep an ordinary PHP method for lower-level domain logic. The ability callback can call that method. This prevents the registry object from becoming the only way your own code can reuse a service and keeps the business layer easy to unit test.
A traditional REST controller still makes sense for a broad resource API with established collection semantics, embedding, pagination and CRUD routes. The Abilities REST API is best at discrete operations. There is no prize for converting a mature, well-designed REST resource into twenty thin abilities unless real consumers need those actions.
Hooks remain the right tool when other code should react to an event or modify a value within the same process. An ability is invoked intentionally. An action such as save_post announces that something already happened. Treating those as interchangeable produces confusing APIs.
Frequently asked questions
Was the WordPress Abilities API introduced in WordPress 7.1?
No. It entered Core in WordPress 6.9. WordPress 7.1 extends it with filtered discovery, unified public metadata, client schema preparation, execution filters, custom validation, invocation telemetry and typed REST inputs. A plugin can register a basic ability on 6.9, but the integration experience is stronger on 7.1.
Does meta.public let anonymous visitors execute an ability?
No. It signals that an ability may be exposed to external channels. Core’s REST endpoints still require an authenticated WordPress user, and every execution must pass the registered permission callback. An adapter may impose another authentication layer as well.
Do I still need show_in_rest in WordPress 7.1?
Not when public is true and the default REST behavior is what you want. Keep show_in_rest when you need an explicit channel override or when the plugin still supports WordPress 6.9 or 7.0 and must expose the ability there.
Does registering an ability automatically make it available in Claude or ChatGPT?
No. The registration creates the WordPress contract. An external client still needs a transport and authentication. For MCP clients, the official WordPress MCP Adapter can expose opted-in abilities. Other clients may call the authenticated Abilities REST API or use a purpose-built adapter.
Should the permission callback check a nonce?
Usually not. A nonce belongs to the HTTP or UI request layer and is not authorization. The permission callback should determine whether the current WordPress user may perform the operation, normally with current_user_can(). Same-origin REST requests handle nonces at the REST authentication layer; Application Password requests use their own credentials.
Can an ability return WP_Error even though it has an output schema?
Yes. WP_Error is the standard failure path and is not treated as a successful output that must match the schema. Successful results must conform to output_schema.
Should every public plugin function become an ability?
No. Register meaningful, bounded operations that another component would intentionally discover and run. Small implementation helpers, formatting functions and event callbacks usually belong in ordinary PHP. A crowded registry full of near-duplicate abilities is harder for humans and automated clients to use safely.
Can one ability belong to several categories?
No. An ability belongs to one registered category. Use the plugin namespace and metadata for additional discovery dimensions rather than cloning the ability across categories.
More on Abilities API integration for plugins
The best reason to adopt the Abilities API is not that AI is fashionable. It is that plugin operations need a better boundary than “call this undocumented PHP function” or “copy the validation from our admin AJAX handler.” A named ability puts the description, schema, permission and execution path in one discoverable place.
WordPress 7.1 fills in the pieces that make that boundary practical. Consumers can find the subset they need. Plugins can express general exposure without conflating it with access. Schemas can leave WordPress in a portable form. Cross-cutting policies can join the execution pipeline without replacing the callback. REST, MCP and future clients can build around the same contract.
Start with one operation that already has two callers. Give it a precise name, a tight schema and an object-aware permission check. Invoke it through WP_Ability, then test the same behavior through the transport you intend to support. That single conversion will show whether the abstraction earns its place in your plugin far more clearly than registering a large catalog in one release.
