Wp Abilities API
WordPressOfficialPublished by WordPress
- Skill
- Tool integration
- Software development
- GPL-2.0-or-later
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured approach to working with the WordPress Abilities API. It covers the registration of abilities and categories in PHP, exposing them via the REST API, and consuming them in JavaScript using @wordpress/abilities. The documentation emphasizes a layered architecture—distinguishing between domain capabilities and their projections—to ensure stable, transport-neutral plugin contracts that remain maintainable as consumer requirements evolve.
What it does
- Registers abilities and categories using the WordPress Abilities API.
- Exposes domain capabilities through the REST API for client consumption.
- Provides patterns for delegating ability execution to existing REST controllers.
- Offers guidance on grouping heuristics to avoid atomic-ability bloat.
- Defines standard error vocabulary for consistent agent-based error handling.
- Facilitates client-side ability consumption via @wordpress/abilities.
When to use it
- When adding new capabilities that should be discoverable by agents or external clients.
- When refactoring existing REST endpoints into a more stable, action-oriented ability contract.
- When building features that require consistent permission checks across different transport layers.
- When debugging issues where abilities are not appearing in REST responses or client-side registries.
Advantages
- Promotes a stable, transport-neutral domain layer that survives UI or consumer changes.
- Reduces boilerplate by providing patterns for delegating to existing controller logic.
- Ensures consistent error handling and input validation across the plugin ecosystem.
- Improves discoverability of plugin actions for AI agents and automated workflows.
Usage examples
- “Register a new ability for listing user orders in my plugin.”
- The agent will guide you through defining the ability ID, category, and meta, ensuring it is registered via the correct hooks and exposed in the REST API.
- “My ability is not showing up in the REST API; how do I debug this?”
- The agent will help you verify the registration hook, check for the show_in_rest meta flag, and confirm the namespace and route configuration.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/wordpress/wp-abilities-api/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip wp-abilities-api.zip -d .agents/skills
Documentation by WordPress
WP Abilities API
When to use
Use this skill when the task involves:
registering abilities or ability categories in PHP,
exposing abilities to clients via REST (
wp-abilities/v1),consuming abilities in JS (notably
@wordpress/abilities),diagnosing “ability doesn’t show up” / “client can’t see ability” / “REST returns empty”.
Inputs required
Repo root (run
wp-project-triagefirst if you haven’t).Target WordPress version(s) and whether this is WP core or a plugin/theme.
Where the change should live (plugin vs theme vs mu-plugin).
Procedure
Before deciding what to register, read references/domain-vs-projection.md — abilities live at the domain capability layer; MCP / Command Palette / REST exposure is a projection. Registration shape and exposure shape are different decisions, and conflating them forces re-registration every time a consumer's constraints change.
1) Confirm availability and version constraints
If this is WP core work, check
signals.isWpCoreCheckoutandversions.wordpress.core.If the project targets WP < 6.9, you may need the Abilities API plugin/package rather than relying on core.
2) Find existing Abilities usage
Search for these in the repo:
wp_register_ability(wp_register_ability_category(wp_abilities_api_initwp_abilities_api_categories_initwp-abilities/v1@wordpress/abilities
If none exist, decide whether you’re introducing Abilities API fresh (new registrations + client consumption) or only consuming.
3) Register categories (optional)
If you need a logical grouping, register an ability category early (see references/php-registration.md).
4) Register abilities (PHP)
For grouping decisions (how many abilities to register, and where to put filters vs. new ability names), read references/grouping-heuristic.md first — it keeps you from shipping one atomic ability per REST operation.
To avoid drift between the ability and the existing UI / REST code path, see references/shared-core-service.md — abilities, REST handlers, CLI commands, and UI controllers should be thin adapters over a shared service. The reference also covers the metric trap (REST handlers that emit usage telemetry) and the AGENTS.md rule for keeping registrations in sync when underlying code paths change.
For shared helper patterns when multiple execute callbacks delegate to existing REST controllers, see references/plugin-family-patterns.md (identify the shared-API-client vs zero-arg-controllers shape) and references/delegate-helper-pattern.md (one helper shape that works, and when not to use it).
For standardized WP_Error codes that let agents reason about retry vs. escalation, see references/error-code-vocabulary.md.
Implement the ability in PHP registration with:
stable
id(namespaced),label/description,category,meta:add
readonly: truewhen the ability is informational,set
show_in_rest: truefor abilities you want visible to clients.
Use the documented init hooks for Abilities API registration so they load at the right time (see references/php-registration.md).
5) Confirm REST exposure
Verify the REST endpoints exist and return expected results (see
references/rest-api.md).If the client still can’t see the ability, confirm
meta.show_in_restis enabled and you’re querying the right endpoint.
6) Consume from JS (if needed)
Prefer
@wordpress/abilitiesAPIs for client-side access and checks.Ensure build tooling includes the dependency and the project’s build pipeline bundles it.
Verification
wp-project-triageindicatessignals.usesAbilitiesApi: trueafter your change (if applicable).REST check (in a WP environment): endpoints under
wp-abilities/v1return your ability and category when expected.If the repo has tests, add/update coverage near:
PHP: ability registration and meta exposure
JS: ability consumption and UI gating
Failure modes / debugging
Ability never appears:
registration code not running (wrong hook / file not loaded),
missing
meta.show_in_rest,incorrect category/ID mismatch.
REST shows ability but JS doesn’t:
wrong REST base/namespace,
JS dependency not bundled,
caching (object/page caches) masking changes.
Execute callback returns unexpected errors or silently ignores input:
input_schemadefaults aren't being applied, pagination key drift between the ability and the backing, orempty()-based ID validation — seereferences/input-schema-gotchas.md.
Escalation
If you’re uncertain about version support, confirm target WP core versions and whether Abilities API is expected from core or as a plugin.
For canonical details, consult:
references/rest-api.mdreferences/php-registration.md
Files
- ATTRIBUTION.mdAdded by AutoGineer896 B
- LICENSELicence file added by AutoGineer750 B
- SKILL.md5.2 KiB
- references/delegate-helper-pattern.md12.2 KiB
- references/domain-vs-projection.md8.9 KiB
- references/error-code-vocabulary.md7.0 KiB
- references/grouping-heuristic.md8.5 KiB
- references/input-schema-gotchas.md12.9 KiB
- references/php-registration.md6.4 KiB
- references/plugin-family-patterns.md11.7 KiB
- references/rest-api.md378 B
- references/shared-core-service.md14.8 KiB
references/plugin-family-patterns.md
Plugin-family patterns
This reference covers shared implementation mechanics — the call shape your execute callbacks follow when handing work to existing business logic. Two shapes are common enough across real-world WordPress plugins to be worth naming; which one fits is determined by how the backing controller is constructed, not by a choice you make up front. The choice still ripples through your delegate helper, your tests, and your error codes, so it's worth confirming early.
Family-specific registration conventions — loader path, ability category, MCP exposure defaults, error-code prefix house style — live in separate plugin-family overlays (for example, a WooCommerce-extension overlay), not in this reference. Apply any relevant overlay before scaffolding registration. The patterns below should stay portable: they describe how the execute callback talks to backing code, not what the registration metadata should look like for your plugin family.
There is also a third option that sits outside this dichotomy. If the operation does not yet have a shared service class — or if you can refactor toward one — extracting a service that the ability, the REST controller, and the UI all consume is the default per shared-core-service.md. Delegation through an existing REST controller (either shape below) is a conditional shortcut for low-stakes reads, not the starting point. Confirm the shape is right before you reach for either.
Shape: shared API client
Common in plugins that talk to a remote service (Stripe, a first-party SaaS, an upstream API). The plugin bootstraps a single API client, exposes it via a static accessor on a main plugin class, and every REST controller takes that client as a constructor argument.
Detection
# Inspect the backing REST controller's __construct signature.
grep -n "public function __construct" <path/to/rest-controller.php>If the constructor takes a required typed argument (e.g. My_Plugin_API_Client $api_client), you're in the shared-API-client shape. Confirm the plugin has a central accessor:
grep -rn "get_api_client\|get_.*_api_client\|get_service" <path/to/plugin/main-class.php>You're looking for a public static function get_<something>() that returns the shared client.
Minimal skeleton
<?php
namespace My_Plugin\Abilities;
class Abilities_Registrar {
const CATEGORY_SLUG = 'my-plugin';
public static function init() {
add_action( 'wp_abilities_api_categories_init', [ __CLASS__, 'register_category' ] );
add_action( 'wp_abilities_api_init', [ __CLASS__, 'register_abilities' ] );
}
public static function register_abilities() {
wp_register_ability(
'my-plugin/get-things',
[
'label' => __( 'Get things', 'my-plugin' ),
'description' => __( 'List things with filters.', 'my-plugin' ),
'category' => self::CATEGORY_SLUG,
'input_schema' => [ /* ... */ ],
'execute_callback' => [ __CLASS__, 'execute_get_things' ],
'permission_callback' => [ __CLASS__, 'current_user_has_capability' ],
'meta' => [
'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ],
'show_in_rest' => true,
],
]
);
}
public static function execute_get_things( $input = null ) {
return self::delegate_to_rest_controller(
'My_Plugin_REST_Things_Controller',
'get_things',
'/my-plugin/v1/things',
is_array( $input ) ? $input : null
);
}
private static function delegate_to_rest_controller( $controller_class, $method, $route, $input, $http_method = 'GET' ) {
$fqcn = '\\' . $controller_class;
if ( ! class_exists( $fqcn ) ) {
return new \WP_Error( 'my_plugin_not_initialized', __( 'My Plugin is not initialized.', 'my-plugin' ) );
}
// Shape specifics: fetch the shared API client and null-check.
$api_client = null;
if ( class_exists( '\My_Plugin' ) && method_exists( '\My_Plugin', 'get_api_client' ) ) {
$api_client = \My_Plugin::get_api_client();
}
if ( null === $api_client ) {
return new \WP_Error( 'my_plugin_not_initialized', __( 'My Plugin is not initialized.', 'my-plugin' ) );
}
$request = new \WP_REST_Request( $http_method, $route );
if ( is_array( $input ) ) {
foreach ( $input as $param => $value ) {
$request->set_param( $param, $value );
}
}
$controller = new $fqcn( $api_client );
$response = $controller->{$method}( $request );
if ( is_wp_error( $response ) ) {
return $response;
}
if ( $response instanceof \WP_REST_Response ) {
$data = $response->get_data();
return is_array( $data ) ? $data : [];
}
return is_array( $response ) ? $response : [];
}
}Testing implications
Bootstrap test doubles need the API client. Unit tests that instantiate controllers must provide a mocked or stubbed client; otherwise the constructor fails with
ArgumentCountError: Too few arguments.The "not initialized" error path is reachable and must be covered. One unit test should stub the accessor to return null and assert the ability returns
<plugin>_not_initialized. This is a common failure during partial bootstraps (WP-CLI with restricted loading, test harnesses, admin pages with conditional autoloading).Integration tests need real or faked HTTP. The backing controller will hit the upstream unless the API client is mocked, so integration harnesses typically route through a fake transport.
Worked example: a plugin that talks to an external SaaS or upstream HTTP service typically follows this pattern. The plugin's main class exposes the API client via a static accessor (e.g. Plugin_Main::get_api_client()); REST controllers extend a base class whose constructor takes that client as a typed argument; the abilities registrar pulls the client through the same accessor before constructing controllers.
Shape: zero-arg controllers
Common in plugins/packages that delegate primarily to WordPress core mechanisms (custom post types, options, meta) and don't maintain a single shared API client. Controllers instantiate cleanly without a dependency graph.
Detection
grep -n "public function __construct" <path/to/rest-controller.php>If the constructor takes no required arguments, or takes only simple scalars you can hardcode at the ability call site (e.g. a post-type string), you're in the zero-arg shape.
Also grep the plugin's main bootstrap class for the absence of a singleton API accessor — if there isn't one, the zero-arg shape is the honest choice.
Minimal skeleton
<?php
namespace My_Plugin\Abilities;
class Abilities_Registrar {
const CATEGORY_SLUG = 'my-plugin';
public static function init() {
add_action( 'wp_abilities_api_categories_init', [ __CLASS__, 'register_category' ] );
add_action( 'wp_abilities_api_init', [ __CLASS__, 'register_abilities' ] );
}
public static function register_abilities() {
wp_register_ability(
'my-plugin/get-things',
[
/* ... */
'execute_callback' => [ __CLASS__, 'execute_get_things' ],
/* ... */
]
);
}
public static function execute_get_things( $input = null ) {
$input = is_array( $input ) ? $input : [];
$endpoint = new \My_Plugin_Things_Endpoint();
$request = new \WP_REST_Request( 'GET', '/my-plugin/v1/things' );
foreach ( [ 'page', 'per_page', 'status', 'search' ] as $key ) {
if ( isset( $input[ $key ] ) ) {
$request->set_param( $key, $input[ $key ] );
}
}
$response = $endpoint->get_items( $request );
if ( is_wp_error( $response ) ) {
return $response;
}
return $response instanceof \WP_REST_Response ? $response->get_data() : $response;
}
}No helper is required — the construction step is a single line and inlining per callback stays readable. If you do extract a helper, it takes a constructor_args parameter because the controller class isn't constant across abilities:
/**
* @param string $controller_class Fully-qualified controller class.
* @param string $method Method to invoke on the request.
* @param string $route REST route used when constructing WP_REST_Request.
* @param array|null $input Ability input (or null).
* @param string $http_method HTTP method. Defaults to 'GET'.
* @param array $constructor_args Optional scalar args for the controller's constructor.
* @return array|\WP_Error
*/
private static function delegate_to_rest_controller(
$controller_class,
$method,
$route,
$input,
$http_method = 'GET',
$constructor_args = array()
) {
/* ... */
}The phpDoc carries the array|\WP_Error return shape; the function signature itself stays untyped on the return so this snippet parses on PHP 7.2+.
See delegate-helper-pattern.md for the full helper signature and guards.
Testing implications
No API-client mock needed. Unit tests instantiate the ability registrar directly and call
execute_*with input arrays.Test at the
wp_get_ability()level when possible. With zero-arg controllers the backing often exists in WordPress core territory (e.g. custom post types), so integration tests can exercise the full pipeline without a fake transport.The
<plugin>_not_initializedfailure mode is less common. It still exists — if a package isn't loaded,class_existson the backing controller still fails — but the surface area is smaller.
Worked example: a plugin whose REST controllers wrap a custom post type, taxonomy, option, or meta typically follows this pattern. Each execute callback constructs its controller inline (e.g. new My_CPT_Endpoint( 'my_cpt' )), passing whatever scalar (post-type slug, taxonomy name) the controller needs. No shared helper in the registrar; the construction step is small enough to inline.
Hybrid cases
A single plugin can legitimately use both patterns across different abilities:
A shared-API-client ability for backing controllers that talk to the upstream service.
A zero-arg ability for backing controllers that only touch WP core (e.g. a CPT-based settings endpoint in the same plugin).
If this happens, the helper in delegate-helper-pattern.md can support both via optional constructor_args — but resist the temptation to over-engineer until you have at least two zero-arg abilities that would share the helper.
Anti-pattern — inventing an API client where there isn't one
Don't introduce a fake shared API client to fit the shared-client helper shape. If the plugin is genuinely stateless / CPT-driven, inline construction is the honest answer. The helper is scaffolding that pays back when you have 4+ abilities sharing the build-request → instantiate → unwrap flow; before that, inline code is faster to read and review.
Picking quickly
| Signal | Likely pattern |
|---|---|
Backing controller's __construct takes an API-client-like object | A |
Plugin has a Plugin::get_*_client() static accessor | A |
| Plugin talks to an external SaaS / upstream HTTP service | A |
Backing controller __construct is zero-arg or only takes scalars | B |
| Backing is a CPT / options / meta wrapper | B |
| Plugin's REST surface wraps WP-core post types, taxonomies, options, or meta | B |
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
The author says: Targets WordPress 7.0+ (PHP 7.4.0+). Filesystem-based agent with bash + node. Some workflows require WP-CLI.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- WordPress
- Publisher
- WordPress (Official)
- Source
- WordPress/agent-skills · skills/wp-abilities-api · commit 7374e6f
- Licence
- GNU General Public License v2.0 or later (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by WordPress or WordPress.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download wp-abilities-api.zip