Updated: April 1, 2026 By: Marios

The Abilities API is the most important WordPress feature that nobody is talking about.
Everyone is focused on the shiny parts of WordPress 7.0 — real-time collaboration, the admin redesign, the AI Connectors screen. Meanwhile, buried underneath all of that is a foundational API that fundamentally changes what WordPress plugins can do and how they’re discovered.
The Abilities API lets any plugin register what it can do in a machine-readable format. That means AI agents, automation tools, other plugins, and future WordPress core features can discover your plugin’s capabilities and use them programmatically — without anyone writing custom integration code.
If you’ve ever wished an AI assistant could just call your plugin’s functions directly instead of you copy-pasting outputs between tools, the Abilities API is how that happens. And setting it up is surprisingly straightforward.
This tutorial walks you through registering your first custom ability from scratch, making it discoverable via REST API, exposing it to AI agents through MCP, and understanding the patterns you’ll use to build more complex abilities.
What the Abilities API actually is
Before we write any code, let’s be clear about what this API does and doesn’t do.
The Abilities API is a central registry. It provides a single, standardized place where WordPress core, plugins, and themes can declare: “here is a thing I can do, here are the inputs it expects, here is what it returns, and here is who’s allowed to use it.”
It doesn’t replace your plugin’s existing logic. Your plugin’s code — the functions that actually create posts, resize images, process payments, whatever — stays exactly where it is. The Abilities API just wraps that logic in a standardized interface so it can be discovered and called consistently.
Think of it like this: your plugin is a restaurant kitchen. The Abilities API is the menu. The kitchen doesn’t change. But now there’s a clear, structured list of what’s available, how to order it, and what to expect when it arrives.
Three core components make up the system. An ability is a self-contained unit of functionality with a unique name, human-readable description, input/output definitions using JSON Schema, permission checks, and an execution callback. A category organizes related abilities into groups. And the registry is the central singleton object that holds everything and provides methods for discovering and querying abilities.
Setting up your environment
You need WordPress 7.0, which includes the Abilities API in core. If you’re on WordPress 6.9, you can install the Abilities API as a standalone plugin from the GitHub repository or require it via Composer.
For this tutorial, I’m using WordPress 7.0 on a local WordPress Studio installation. Everything we build will work the same way on any WordPress 7.0 site.
Create a new plugin folder in your wp-content/plugins directory. I’ll call mine content-toolkit. Inside it, create a single PHP file called content-toolkit.php. Here’s the plugin header:
php
<?php
/**
* Plugin Name: Content Toolkit
* Description: Custom abilities for content management
* Version: 1.0.0
*/
Activate the plugin in your WordPress admin. It won’t do anything yet, but it’s ready for us to start registering abilities.
Registering your first ability: a word counter
Let’s start with the simplest possible ability — one that takes a post ID and returns the word count. This is useful for content auditing, editorial dashboards, and AI-driven content analysis.
Step 1: Register a category
Every ability must belong to a category. Categories organize abilities into logical groups. Add this to your plugin file:
php
add_action( 'wp_abilities_api_categories_init', function() {
wp_register_ability_category(
'content-toolkit-analysis',
array(
'label' => __( 'Content Analysis', 'content-toolkit' ),
'description' => __( 'Abilities for analyzing content.', 'content-toolkit' ),
)
);
});
Note the hook: wp_abilities_api_categories_init. Categories must be registered on this specific action — registering them elsewhere will trigger a doing_it_wrong notice and silently fail. The category slug uses dashes only, no slashes or other special characters.
Step 2: Register the ability
Now register the word count ability on the wp_abilities_api_init action:
php
add_action( 'wp_abilities_api_init', function() {
wp_register_ability(
'content-toolkit/count-words',
array(
'label' => __( 'Count Words', 'content-toolkit' ),
'description' => __( 'Returns the word count for a given post.', 'content-toolkit' ),
'category' => 'content-toolkit-analysis',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'post_id' => array(
'type' => 'integer',
'description' => 'The ID of the post to count words for.',
),
),
'required' => array( 'post_id' ),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'post_id' => array( 'type' => 'integer' ),
'title' => array( 'type' => 'string' ),
'word_count' => array( 'type' => 'integer' ),
),
),
'execute_callback' => function( $input ) {
$post = get_post( $input['post_id'] );
if ( ! $post ) {
return new WP_Error(
'post_not_found',
__( 'Post not found.', 'content-toolkit' ),
array( 'status' => 404 )
);
}
return array(
'post_id' => $post->ID,
'title' => $post->post_title,
'word_count' => str_word_count( wp_strip_all_tags( $post->post_content ) ),
);
},
'permission_callback' => function() {
return current_user_can( 'edit_posts' );
},
'meta' => array(
'show_in_rest' => true,
'annotations' => array(
'readonly' => true,
),
'mcp' => array(
'public' => true,
'type' => 'tool',
),
),
)
);
});
Let’s break down every piece.
The name follows the namespace/ability-name pattern. Your namespace (content-toolkit) should match your plugin slug. The ability name (count-words) describes what it does.
The input_schema and output_schema use JSON Schema format. The input schema validates what the ability accepts — in this case, a required integer called post_id. The output schema documents what the ability returns. If someone passes invalid input (like a string instead of an integer for post_id), the API automatically rejects the request with a validation error. You don’t write validation code yourself.
The execute_callback contains your actual business logic. It receives the validated input, does its work, and returns either a result or a WP_Error. This is where your existing plugin functions live — the Abilities API just provides a standardized wrapper.
The permission_callback controls who can use this ability. Here, any user who can edit posts is allowed. For more sensitive abilities, you might require manage_options (administrators only) or a custom capability.
The meta array is where it gets powerful. The show_in_rest flag exposes this ability through the WordPress REST API, meaning you can call it via HTTP. The annotations describe the ability’s behavior — readonly means it doesn’t modify anything, which helps AI agents understand what’s safe to call. And the mcp section makes this ability discoverable by AI agents through the Model Context Protocol.
Step 3: Test it
Save the file and reload your WordPress admin. Your ability is now registered. You can verify it in three ways.
Via PHP anywhere in WordPress:
php
$ability = wp_get_ability( 'content-toolkit/count-words' );
$result = $ability->execute( array( 'post_id' => 1 ) );
Via the REST API:
POST /wp-json/wp/v2/abilities/content-toolkit/count-words/execute
Content-Type: application/json
{"post_id": 1}
Via an AI agent (if you have the MCP Adapter installed and connected): just ask Claude “How many words are in post 1 on my WordPress site?” Claude will discover the count-words ability, call it, and return the result.
Building a more useful ability: content freshness auditor
Now that you understand the pattern, let’s build something genuinely useful. This ability audits all published posts and returns a list of those that haven’t been updated in a specified number of days. Content teams and SEO professionals would find this valuable — and AI agents can use it to proactively suggest content refreshes.
php
add_action( 'wp_abilities_api_init', function() {
wp_register_ability(
'content-toolkit/stale-content-audit',
array(
'label' => __( 'Stale Content Audit', 'content-toolkit' ),
'description' => __( 'Returns published posts not updated within a given number of days.', 'content-toolkit' ),
'category' => 'content-toolkit-analysis',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'days_threshold' => array(
'type' => 'integer',
'description' => 'Number of days. Posts not modified within this period are considered stale.',
'default' => 90,
),
'post_type' => array(
'type' => 'string',
'description' => 'The post type to audit.',
'default' => 'post',
),
'limit' => array(
'type' => 'integer',
'description' => 'Maximum number of results to return.',
'default' => 20,
),
),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'total_stale' => array( 'type' => 'integer' ),
'threshold' => array( 'type' => 'string' ),
'posts' => array(
'type' => 'array',
'items' => array(
'type' => 'object',
'properties' => array(
'id' => array( 'type' => 'integer' ),
'title' => array( 'type' => 'string' ),
'days_since' => array( 'type' => 'integer' ),
'last_modified' => array( 'type' => 'string' ),
'url' => array( 'type' => 'string' ),
),
),
),
),
),
'execute_callback' => function( $input ) {
$days = $input['days_threshold'] ?? 90;
$type = $input['post_type'] ?? 'post';
$limit = $input['limit'] ?? 20;
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( "-{$days} days" ) );
$query = new WP_Query( array(
'post_type' => $type,
'post_status' => 'publish',
'posts_per_page' => $limit,
'date_query' => array(
array(
'column' => 'post_modified_gmt',
'before' => $cutoff,
),
),
'orderby' => 'modified',
'order' => 'ASC',
) );
$results = array();
foreach ( $query->posts as $post ) {
$modified = strtotime( $post->post_modified_gmt );
$results[] = array(
'id' => $post->ID,
'title' => $post->post_title,
'days_since' => (int) floor( ( time() - $modified ) / DAY_IN_SECONDS ),
'last_modified' => $post->post_modified,
'url' => get_permalink( $post->ID ),
);
}
return array(
'total_stale' => count( $results ),
'threshold' => "{$days} days",
'posts' => $results,
);
},
'permission_callback' => function() {
return current_user_can( 'edit_others_posts' );
},
'meta' => array(
'show_in_rest' => true,
'annotations' => array(
'readonly' => true,
),
'mcp' => array(
'public' => true,
'type' => 'tool',
),
),
)
);
});
This ability is more sophisticated. It accepts optional parameters with sensible defaults (90 days, post type, result limit), performs a targeted WP_Query, and returns structured data that’s immediately useful. An AI agent connected via MCP could use this proactively — “Check my site for stale content and suggest which posts to update first.”
Notice the permission_callback requires edit_others_posts instead of just edit_posts. Since this ability returns data about all published posts (not just the current user’s), it should only be available to editors and administrators.
The JavaScript side: client-side abilities in WordPress 7.0
WordPress 7.0 extends the Abilities API to JavaScript. This means you can register and execute abilities on the client side — directly in the block editor, in admin pages, or in any JavaScript context within WordPress.
javascript
import { registerAbility, executeAbility } from '@wordpress/abilities';
import { currentUserCan } from '@wordpress/core-data';
registerAbility( {
name: 'content-toolkit/analyze-readability',
label: 'Analyze Readability',
description: 'Calculates a readability score for the current block content.',
category: 'content-toolkit-analysis',
permissionCallback: () => {
return currentUserCan( 'edit_posts' );
},
callback: async ( input ) => {
const { content } = input;
const words = content.split( /\s+/ ).filter( Boolean ).length;
const sentences = content.split( /[.!?]+/ ).filter( Boolean ).length;
const avgWordsPerSentence = sentences > 0 ? words / sentences : 0;
let level = 'easy';
if ( avgWordsPerSentence > 20 ) level = 'difficult';
else if ( avgWordsPerSentence > 14 ) level = 'moderate';
return {
words,
sentences,
avg_words_per_sentence: Math.round( avgWordsPerSentence * 10 ) / 10,
readability: level,
};
},
} );
Client-side abilities are fetched from the REST API and registered in the client store automatically when the page loads. If an ability exists in both PHP and JavaScript, they share the same registry namespace — meaning an AI agent that discovers your abilities through MCP can see both server-side and client-side capabilities.
To execute a client-side ability from JavaScript:
javascript
const result = await executeAbility( 'content-toolkit/analyze-readability', {
content: 'Your text content here.',
} );
This opens up scenarios like a “readability score” indicator in the block editor sidebar that updates as you type, powered by a registered ability that any other plugin or AI agent could also call.
Connecting abilities to AI agents via MCP
This is where everything comes together. Once your abilities are registered with the mcp.public meta flag, the MCP Adapter exposes them to AI agents automatically.
The flow works like this: you register an ability with mcp.public set to true. The MCP Adapter discovers all public abilities. When an AI agent connects to your site, it calls a discovery tool that returns the list of available abilities. The agent sees your content-toolkit/stale-content-audit ability, understands its inputs and outputs from the JSON Schema, and can call it when relevant.
In practice, this means you can connect Claude Desktop to your site and say: “Run a stale content audit on my blog — find anything not updated in 60 days.” Claude discovers the ability, calls it with the right parameters, and returns your formatted results.
The key insight is that you don’t write any MCP code. The MCP Adapter handles the protocol translation. All you do is register an ability with the right meta flags, and AI agents can use it.
Patterns and best practices from building real abilities
After building abilities across several plugins over the past few months, here’s what I’ve learned about what works well.
Keep abilities focused. One ability should do one thing. Don’t build a “manage-content” ability that creates, updates, deletes, and lists posts. Build four separate abilities: create-post, update-post, delete-post, list-posts. Focused abilities are easier for AI agents to understand and use correctly.
Use annotations honestly. The readonly, destructive, and idempotent annotations aren’t just documentation — they help AI agents make decisions about what’s safe to call. If your ability modifies data, don’t mark it as readonly. If calling it twice with the same input has side effects, don’t mark it as idempotent. AI agents use these flags to determine whether they need to ask for confirmation before executing.
Default to restrictive permissions. Start with manage_options (admin only) for new abilities and relax permissions only after you’re confident the ability is safe. Remember: MCP clients execute abilities as authenticated WordPress users. If the user role has permission to delete posts, an AI agent connected as that user can delete posts through your ability.
Include sensible defaults in your input schema. The stale-content-audit ability defaults to 90 days, “post” type, and 20 results. This means an AI agent can call it with zero parameters and get useful results. Good defaults reduce friction for both human users and AI agents.
Return structured data, not formatted text. Your execute_callback should return arrays and objects, not HTML or formatted strings. The consumer (whether it’s PHP code, a REST API client, a JavaScript UI, or an AI agent) will format the output for its own context. Let the data be the data.
Use WP_Error for failure cases. Don’t return null or false on errors. Return a WP_Error with a meaningful error code, message, and status code. This gives API consumers (and AI agents) enough information to understand what went wrong and potentially retry or adjust their request.
What to build next
Now that you understand the pattern, here are ability ideas that would add real value to a WordPress site and be immediately useful to AI agents:
A broken links checker that scans post content for dead URLs and returns a report. A media library auditor that finds unused images consuming storage. A taxonomy cleanup tool that identifies duplicate or unused tags. A plugin compatibility checker that tests for known conflicts with the current WordPress version. A content similarity finder that compares a draft against published posts to flag potential duplication.
Each of these follows the same pattern: register a category, register an ability with clear input/output schemas and permission checks, implement the logic in your execute_callback, and flag it for REST and MCP access. The Abilities API handles discovery, validation, permissions, and protocol translation. You just write the business logic.