WordPress 6.9 shipped an API for letting AI agents operate your site. Here is the code, and the risk.
On this page
WordPress 6.9 shipped the Abilities API. It is a registry that lets plugins and themes declare units of functionality with defined inputs, outputs, and permissions, so that other software can discover and execute them through a standard interface.
The phrase other software is doing a lot of work in that sentence. In practice the consumer is an AI agent. This is WordPress building the front door that agents will walk through, and it is worth understanding before something else registers abilities on a site you maintain.
Registering an ability
The minimum viable ability is short. Abilities register on the wp_abilities_api_init hook, not on init, because the registry has to exist first.
Ad space, reserved
add_action( 'wp_abilities_api_init', 'techist_register_abilities' );
function techist_register_abilities() {
wp_register_ability(
'techist/get-site-title',
array(
'label' => __( 'Get Site Title', 'techist' ),
'description' => __( 'Retrieves the title of the current site.', 'techist' ),
'output_schema' => array(
'type' => 'string',
'description' => 'The site title.',
),
'execute_callback' => 'techist_get_site_title',
'permission_callback' => '__return_true',
'meta' => array(
'category' => 'site-info',
'show_in_rest' => true,
),
)
);
}
function techist_get_site_title() {
return get_bloginfo( 'name' );
}The namespace prefix is not decorative. Ability names are global, so techist/get-site-title and someotherplugin/get-site-title coexist. Use your plugin slug and do not skip it.
Categories register first
If you reference a category in meta, it has to exist already. Categories register on their own earlier hook.
add_action( 'wp_abilities_api_categories_init', function () {
wp_register_ability_category(
'site-info',
array(
'label' => __( 'Site information', 'techist' ),
'description' => __( 'Read-only facts about this site.', 'techist' ),
)
);
} );Get the order wrong and the ability registers against a category that does not exist. It will not fatal. It will just be harder to discover, which is worse, because you will not notice.
Taking input, and validating it
An ability that accepts arguments declares an input_schema, and WordPress validates against it automatically before your callback runs. This is the part that makes the API worth using rather than exposing your own REST route.
wp_register_ability(
'techist/summarise-post',
array(
'label' => __( 'Summarise a post', 'techist' ),
'input_schema' => array(
'type' => 'object',
'properties' => array(
'post_id' => array(
'type' => 'integer',
'minimum' => 1,
),
'max_words' => array(
'type' => 'integer',
'minimum' => 20,
'maximum' => 300,
'default' => 80,
),
),
'required' => array( 'post_id' ),
),
'output_schema' => array(
'type' => 'string',
),
'execute_callback' => 'techist_summarise_post',
'permission_callback' => function ( $input ) {
return current_user_can( 'edit_post', $input['post_id'] );
},
'meta' => array( 'category' => 'content', 'show_in_rest' => true ),
)
);Note what the permission_callback receives. It gets the validated input, which means you can make the permission decision against the specific object being acted on rather than a blanket capability check. That distinction is the whole security story of this API, and it is the thing most examples online get lazy about.
Guard for older WordPress
If your plugin runs anywhere other than sites you control, the function may not exist. This is cheap insurance.
add_action( 'wp_abilities_api_init', function () {
if ( ! function_exists( 'wp_register_ability' ) ) {
return;
}
// registration goes here
} );How abilities get executed
Registered abilities are exposed under the wp-abilities/v1 REST namespace, with execution at a run endpoint on the individual ability. Access to all Abilities REST endpoints requires an authenticated user, which is the correct default.
In PHP you retrieve the ability object and execute it:
$ability = wp_get_ability( 'techist/summarise-post' );
if ( $ability ) {
$result = $ability->execute( array(
'post_id' => 42,
'max_words' => 60,
) );
}The part that should make you careful
Everything above is a good API. My concern is not the design, it is the deployment pattern it invites.
An ability is, by construction, a thing an autonomous agent can find and call without a human choosing it from a menu. The permission_callback is therefore not a UI convenience. It is the only thing standing between a registered function and an agent that has decided calling it would be helpful.
Three rules I would apply to any ability before shipping it:
- Never use
__return_trueon anything that writes. It is fine for reading a site title. It is not fine for anything with a verb in its name. - Check the capability against the specific object, not the general one.
current_user_can( 'edit_post', $id ), notcurrent_user_can( 'edit_posts' ). - Do not register destructive abilities at all. Deleting, bulk-updating, and changing settings are things a human should initiate. An agent that can plausibly justify calling delete-all-drafts eventually will.
I have a direct reason to hold that last opinion. Earlier this month an agent operating this very site wrote a PHP file in chunks, a write failed partway, and the site went down with a fatal error. The tooling was working exactly as designed. The failure was that a multi-step write had no atomicity, so a partial result was indistinguishable from a complete one.
Abilities have the same shape of risk. Design each one so that calling it twice is safe, calling it with garbage is rejected by the schema, and calling it without the right capability is refused before your code runs. Then it does not much matter whether the caller is a person or a model.
Sources
- Abilities API, getting started, WordPress Developer Resources
- Abilities API in WordPress 6.9, Make WordPress Core
- Introducing the WordPress Abilities API
Ad space, reserved




