Generated banner for Wp Abilities API
Generated logo for Wp Abilities API

Wp Abilities API

WordPressOfficialPublished by WordPress

  • Skill
  • Tool integration
  • Software development
  • GPL-2.0-or-later
  • No issues found

From the author

Use when working with the WordPress Abilities API (wp_register_ability, wp_register_ability_category, /wp-json/wp-abilities/v1/*, @wordpress/abilities) including defining abilities, categories, meta, REST exposure, and permissions checks for clients.
— the description by WordPress

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/wordpress/wp-abilities-api/download
  2. Unzip 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-triage first 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.isWpCoreCheckout and versions.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_init

  • wp_abilities_api_categories_init

  • wp-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: true when the ability is informational,

    • set show_in_rest: true for 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_rest is enabled and you’re querying the right endpoint.

6) Consume from JS (if needed)
  • Prefer @wordpress/abilities APIs for client-side access and checks.

  • Ensure build tooling includes the dependency and the project’s build pipeline bundles it.

Verification

  • wp-project-triage indicates signals.usesAbilitiesApi: true after your change (if applicable).

  • REST check (in a WP environment): endpoints under wp-abilities/v1 return 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_schema defaults aren't being applied, pagination key drift between the ability and the backing, or empty()-based ID validation — see references/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.md

    • references/php-registration.md

Files

references/delegate-helper-pattern.md

Delegate helper pattern

Once you've decided delegation is the right shape for an ability — that is, the backing REST controller is a pure data-fetch, the operation is a read, and the ability runs predominantly inside REST contexts (see shared-core-service.md for when the answer is "no, extract a shared service" instead) — extract a delegate_to_rest_controller helper rather than open-coding the request-build/dispatch/unwrap pipeline in each execute callback. This reference documents one helper shape that works, the three guards every implementation needs, the dual response-shape unwrap, and when NOT to use the helper at all. Adapt the exact signature to the plugin you're working in — the guards and unwrap matter more than the parameter list.

Read plugin-family-patterns.md first — the helper's exact shape depends on how the backing controller is constructed (shared API client vs. zero-arg).

Why this helper exists

List-style read abilities typically repeat the same four steps in every execute callback:

  1. Validate the plugin is initialized (class exists + dependencies resolved).

  2. Build a WP_REST_Request and copy the ability's $input onto it as params.

  3. Instantiate the backing controller and invoke the named method.

  4. Unwrap the response (controllers return either a raw array or a WP_REST_Response).

Four abilities × this repetition = the boilerplate dominates the callback body. Extracting a helper keeps each execute callback a 3–4 line function that reads like pseudocode.

When to extract

After the second list-style ability lands. Before the third ability gets added, refactor the duplicated code out of the first two execute callbacks into a private static helper. Convert subsequent abilities to call the helper as you add them.

If the plugin only ever ships one or two abilities, inline the code. The helper's payoff is at 3+ callers.

Helper shape

/**
 * Delegate an ability's execute callback to a REST controller.
 *
 * Builds a WP_REST_Request from the ability's input, instantiates the
 * backing controller with the plugin's shared API client, invokes the
 * named method, and normalizes the return so callers always see
 * array|\WP_Error. Controllers in this plugin return either a
 * WP_REST_Response or a raw array; the helper unwraps both shapes.
 *
 * Not used by zero-arg abilities — those call their controller directly
 * so we don't synthesize a WP_REST_Request just to discard it.
 *
 * @param string     $controller_class Fully-qualified controller class (no leading backslash).
 * @param string     $method           Controller method to invoke on the built request.
 * @param string     $route            REST route string used when constructing WP_REST_Request.
 * @param array|null $input            Ability input; each key/value becomes a request param.
 * @param string     $http_method      HTTP method for WP_REST_Request. Defaults to 'GET'; writes pass 'POST'.
 * @return array|\WP_Error             Response payload as an array, or WP_Error on failure.
 */
private static function delegate_to_rest_controller(
    $controller_class,
    $method,
    $route,
    $input,
    $http_method = 'GET'
) {
    $fqcn = '\\' . $controller_class;

    // Guard 1: controller class is loaded.
    if ( ! class_exists( $fqcn ) ) {
        return new \WP_Error(
            '<plugin>_not_initialized',
            __( '<Plugin> is not initialized.', '<text-domain>' )
        );
    }

    // Guard 2: shared API client is available.
    $api_client = null;
    if ( class_exists( '\<Plugin_Main_Class>' ) && method_exists( '\<Plugin_Main_Class>', 'get_<client_accessor>' ) ) {
        $api_client = \<Plugin_Main_Class>::get_<client_accessor>();
    }
    if ( null === $api_client ) {
        return new \WP_Error(
            '<plugin>_not_initialized',
            __( '<Plugin> is not initialized.', '<text-domain>' )
        );
    }

    // Build the request.
    $request = new \WP_REST_Request( $http_method, $route );
    if ( null !== $input ) {
        foreach ( $input as $param => $value ) {
            $request->set_param( $param, $value );
        }
    }

    // Invoke + guard 3: WP_Error short-circuit + dual-shape unwrap.
    $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 : [];
}

Positional (not named) arguments on purpose — the helper is called from every list ability and keyword-args for PHP 8 would add visual noise. Order is intentional: controller, method, route, input, http_method (the defaultable tail).

The three guards

1. class_exists on the controller

Execute callbacks can be reached on sites where the plugin's classes haven't loaded (tests, WP-CLI with limited bootstrap, admin pages with conditional autoloading). Check is cheap; without it a missing class produces a fatal.

Note '\\' . $controller_class — accept controller class names without a leading backslash (readable in config arrays) and re-add it so class_exists and new $fqcn(...) both root-resolve.

2. Required dependency (API client) null-check

When the controller takes a shared API client as a constructor argument, the accessor is typically a public static method on the main plugin class. Guard both class_exists on the plugin class AND method_exists on the accessor because either could be absent during partial bootstraps. Treat a null return from the accessor as "not initialized".

Missing this argument produces ArgumentCountError: Too few arguments to function <Controller>::__construct(), 0 passed — a PHP fatal. The standardized <plugin>_not_initialized error code is documented in error-code-vocabulary.md.

When the controller takes no constructor args, skip this guard entirely. When it takes only simple scalars (e.g. a post-type string), pass them in via an optional constructor_args parameter on the helper.

3. is_wp_error short-circuit

Before trying to unwrap the response, check if it's already a WP_Error. Several controllers return WP_Error for both validation failures and upstream failures; that error needs to bubble up intact so the agent can see the upstream code.

Dual response-shape unwrap

if ( $response instanceof \WP_REST_Response ) {
    $data = $response->get_data();
    return is_array( $data ) ? $data : [];
}
return is_array( $response ) ? $response : [];

Real WordPress REST controllers return either:

  • A WP_REST_Response (the output of rest_ensure_response( ... ) or new WP_REST_Response( ... )), or

  • A raw array (typical when a controller delegates to an internal request-handler class that returns the decoded transport payload directly).

Both happen. The helper unwraps WP_REST_Response via get_data() and passes raw arrays through. The is_array(...) ? ... : [] coalesce defends against a non-array return type that the ability's output_schema would have rejected downstream anyway — returning [] fails safely rather than leaking a surprising type.

When NOT to use the helper

Three categories of abilities bypass the helper and call the backing directly:

Backing request class can be invoked without the controller

If the REST controller's only role is to translate WP_REST_Request into a call to an underlying request class (e.g., Things_Request::from_rest_request( $request )->execute()) and adds no validation, permission logic, or orchestration on top, bypass the controller and call the request class directly:

public static function execute_get_things( $input = null ) {
    if ( ! class_exists( '\My_Plugin\Things_Request' ) ) {
        return new \WP_Error( '<plugin>_not_initialized', __( '<Plugin> is not initialized.', '<text-domain>' ) );
    }

    $request = new \WP_REST_Request( 'GET', '/my-plugin/v1/things' );
    foreach ( (array) $input as $param => $value ) {
        $request->set_param( $param, $value );
    }

    $things_request = \My_Plugin\Things_Request::from_rest_request( $request );
    $rows           = $things_request->execute();

    return is_array( $rows ) ? $rows : [];
}

The WP_REST_Request object exists only to satisfy from_rest_request()'s parameter contract — nothing dispatches it. This is the shortest path through the layers: no controller construction, no controller-emitted side effects, and (when the request class is hit before any REST traffic in the lifecycle) no rest_api_init cost. See shared-core-service.md for the broader discussion of REST-path side effects and the first-call bootstrap cost on cold paths.

The tradeoff is real: bypassing the controller bypasses anything the controller does. If the controller runs validation that the request class doesn't, or emits an audit hook the request class doesn't, that work is lost on the ability path. Use this shape when the controller is genuinely a thin wrapper, not when it's doing work you'd want to keep.

Zero-arg backing methods

If the backing method takes no WP_REST_Request parameter, don't synthesize a request just to discard it. Call the method directly:

public static function execute_get_overview( $input = null ) {
    if ( ! class_exists( '\My_Plugin_REST_Overview_Controller' ) ) {
        return new \WP_Error( '<plugin>_not_initialized', __( '<Plugin> is not initialized.', '<text-domain>' ) );
    }

    $api_client = /* ... same accessor check ... */;
    if ( null === $api_client ) {
        return new \WP_Error( '<plugin>_not_initialized', __( '<Plugin> is not initialized.', '<text-domain>' ) );
    }

    $controller = new \My_Plugin_REST_Overview_Controller( $api_client );
    $response   = $controller->get_overview(); // No $request argument.

    if ( is_wp_error( $response ) ) {
        return $response;
    }
    return is_array( $response ) ? $response : [];
}
Abilities that use a non-REST service

If the execute callback reaches a service directly (e.g. My_Plugin::get_account_service()->get_cached_account_data()) rather than a REST controller, stay on the direct path. The ability is not REST-backed and the helper would add a construction step for no gain.

Rule of thumb

If the backing method takes WP_REST_Request and the controller adds something beyond delegating to an underlying request class (validation, permissions, orchestration, side effects you want to keep), use the helper. If the controller is a thin wrapper over a request class, call the request class directly. Otherwise direct-path.

Conversion pattern — before and after

Before extraction (inline in the execute callback):

public static function execute_get_things( $input = null ) {
    if ( ! class_exists( '\My_Plugin_REST_Things_Controller' ) ) {
        return new \WP_Error( '<plugin>_not_initialized', __( '<Plugin> is not initialized.', '<text-domain>' ) );
    }
    $api_client = \My_Plugin::get_api_client();
    if ( ! $api_client ) {
        return new \WP_Error( '<plugin>_not_initialized', __( '<Plugin> is not initialized.', '<text-domain>' ) );
    }
    $request = new \WP_REST_Request( 'GET', '/my-plugin/v1/things' );
    foreach ( (array) $input as $param => $value ) {
        $request->set_param( $param, $value );
    }
    $controller = new \My_Plugin_REST_Things_Controller( $api_client );
    $response   = $controller->get_things( $request );
    // ... unwrap ...
}

After extraction:

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
    );
}

Note the is_array( $input ) ? $input : null — the helper signature is ?array, and passing null tells it to build the request with no params at all. This matters because the Abilities API sometimes invokes a callback with no input object.

  • plugin-family-patterns.md — choosing the helper's constructor shape.

  • error-code-vocabulary.md — the <plugin>_not_initialized code and its siblings.

  • input-schema-gotchas.md — callbacks for list abilities often need per_page pagination translation BEFORE calling the helper.

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.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download wp-abilities-api.zip