Until now, every plugin that wanted an AI agent (or any outside tool) to do something on a WordPress site had to invent its own REST endpoints, its own permission checks and its own way of describing what they do. WordPress 6.9 added the Abilities API , a single registry where plugins describe what they can do in a format both code and AI agents can read.
An ability is one named action: a label, a description, input and output schemas, a permission check, and the callback that does the work. This issue registers a small read-only one that counts draft posts.
1. Register a category
Every ability belongs to a category, so register that first:
add_action( 'wp_abilities_api_categories_init', function () {
wp_register_ability_category( 'hsr-content', array(
'label' => __( 'Content', 'hsr' ),
'description' => __( 'Abilities that read site content.', 'hsr' ),
) );
} );
Category slugs use lowercase letters, numbers and dashes only.
2. Register the ability
add_action( 'wp_abilities_api_init', function () {
wp_register_ability( 'hsr/count-drafts', array(
'label' => __( 'Count drafts', 'hsr' ),
'description' => __( 'Returns the number of draft posts on the site.', 'hsr' ),
'category' => 'hsr-content',
'output_schema' => array(
'type' => 'object',
'properties' => array(
'drafts' => array( 'type' => 'integer' ),
),
),
'execute_callback' => function () {
$counts = wp_count_posts( 'post' );
return array( 'drafts' => (int) $counts->draft );
},
'permission_callback' => function () {
return current_user_can( 'edit_posts' );
},
'meta' => array(
'show_in_rest' => true,
'annotations' => array( 'readonly' => true ),
),
) );
} );
The array passed to wp_register_ability() reads top to bottom as four groups: what the ability is called, what it returns, what it does, and who’s allowed to run it.
Naming it. The first argument, hsr/count-drafts, is the ability’s unique name. It has to follow the namespace/ability-name format, and the namespace should be your plugin’s slug so it can’t clash with anyone else’s.
label is the short human-readable name a UI would show. description is the longer explanation, and it matters more than it looks: when an AI agent is choosing which ability to call, this is the text it reads. Write it for someone who has never seen your code.
category must match a slug you’ve already registered, which is why section 1 comes first. If it doesn’t match, WordPress won’t register the ability and you’ll find a “doing it wrong” notice in debug.log.
Describing the result. output_schema is a JSON Schema describing what the ability returns. Here it says “an object with one key, drafts, which is an integer.”
This isn’t just documentation. WordPress checks the callback’s return value against this schema, and if it doesn’t match, the caller gets an error instead of the result.
This ability takes no input, so there’s no input_schema. When an ability does need input, that key describes it in the same JSON Schema format and WordPress validates it before your callback runs.
Doing the work. execute_callback is the function that actually runs. wp_count_posts( 'post' ) returns an object with one property per post status, and we pick out draft.
The (int) cast is there because of the schema. The counts come from the database as strings ("3", not 3), and a string would fail the integer check.
Checking permission. permission_callback runs before execute_callback, every time. If it returns false, the callback never runs. Here it requires edit_posts, which contributors and every role above them have.
The extra settings. meta holds options that sit outside the core definition. show_in_rest makes the ability visible and runnable through the REST API, and is off by default. The readonly annotation tells callers (including AI agents) that running it won’t change anything on the site.
Warning: with show_in_rest on, the permission callback is the only thing between this ability and anyone who can reach your REST API.
Returning true from it means every visitor succeeds, logged in or not.
For a draft count that’s a small leak. For an ability that updates posts or reads customer data, it’s a real hole, so always check a specific capability.
3. Run it
From WP-CLI, as a user who can edit posts:
wp eval 'print_r( wp_get_ability( "hsr/count-drafts" )->execute() );' --user=admin
You should see an array with a drafts key and your actual draft count. Run it without --user and the permission check fails, which is a quick way to confirm the callback is doing its job.
Over REST, with an application password:
curl -u admin:YOUR_APP_PASSWORD https://yoursite.test/wp-json/wp-abilities/v1/abilities/hsr/count-drafts/run
Same result, as JSON. That same registered ability is what the MCP Adapter hands to AI agents like Claude, which I’ll cover in a later issue.
That’s all for this one.
Try it: register a second read-only ability that returns the number of pending comments (wp_count_comments() will get you there). Tell me what you wrote for its description, I’m curious how different everyone’s are.
The post Register your first WordPress ability first appeared on alokjain.dev.

Top comments (0)