Claude Skill

wp-abilities-api

Register WordPress Abilities: machine-readable plugin operations with JSON Schema contracts, required permission callbacks, optional REST exposure, client-side abilities, and AI/MCP-friendly discovery. Covers categories, wp_register_ability, WP_Ability::execute, REST run endpoint

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lonsdale201-wp-agent-skills-wordpress_wp-abilities-api-52f6020.zip · 10 KB
Part of lonsdale201/wp-agent-skills — 226 skills

Install

skills CLI npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/wordpress/wp-abilities-api
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
Git git clone https://github.com/Lonsdale201/wp-agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

WordPress Abilities API

A standardized registry for plugin / theme / core functionality, designed primarily so AI agents and external tools can discover what a WordPress site can do and invoke those capabilities through a uniform contract. Each Ability has a stable identifier, JSON-Schema-typed inputs and outputs, and a required permission callback. WP_Ability::execute() validates input, checks permissions, executes, and validates output.

Pre-Abilities, the same functionality was scattered across do_action, apply_filters, custom REST routes, public PHP functions, and ad-hoc plugin APIs (each with its own conventions). The Abilities API consolidates that into one machine-readable surface.

This skill is grounded in WordPress 7.1 core behavior. Server-side Abilities shipped in WordPress 6.9; WordPress 7.0 added the client packages and WP AI Client integration. WordPress 7.1 adds unified public exposure metadata, filtered discovery, a complete execution-filter lifecycle, client-safe JSON Schema preparation, REST collection filters/pagination, and schema-aware run-input coercion. Verify version-sensitive behavior against wp-includes/abilities-api, the WP_REST_Abilities_V1_* controllers, and the abilities script modules.

When to use this skill

Trigger when ANY of the following is true:

  • Scaffolding plugin functionality that AI agents (Claude, ChatGPT, custom MCP clients) should be able to discover and invoke.
  • Designing an admin tool whose logic should be reachable from BOTH PHP code and the block editor JS without writing two parallel implementations.
  • Reviewing a plugin where you see wp_register_ability, wp_get_ability, WP_Ability, the wp_abilities_api_init action, or the @wordpress/abilities JS package.
  • Registering client-side admin operations with @wordpress/abilities, or loading server abilities into JS with @wordpress/core-abilities.
  • Passing abilities to wp_ai_client_prompt()->using_abilities() or handling WP_AI_Client_Ability_Function_Resolver.
  • Deciding whether to expose a feature as a custom REST endpoint vs an Ability.
  • Migrating an existing custom REST endpoint to the Abilities API.

Availability and installation

Three installation paths, depending on WP version:

  1. WordPress 7.0+ - server-side PHP API plus client-side @wordpress/abilities and @wordpress/core-abilities script modules ship in core. Feature-detect 7.1 additions before using them on 7.0.
  2. WordPress 6.9.x - server-side PHP API, registry, REST exposure, and core integration ship in core. Feature-detect the JS packages before relying on them.
  3. WordPress < 6.9 - Composer package for plugins that bundle their own dependencies:
    composer require wordpress/abilities-api
    
    Package: https://packagist.org/packages/wordpress/abilities-api.
  4. WordPress < 6.9 - feature plugin for site-wide install: download the latest release ZIP from https://github.com/WordPress/abilities-api or install via WP admin.

For JavaScript, prefer script modules and explicit dependencies. Do not assume a window.wp.abilities global exists.

Registering a category (prerequisite)

Every Ability MUST belong to a category. Register categories on the dedicated init action (Abilities API docs):

add_action( 'wp_abilities_api_categories_init', 'myplugin_register_ability_categories' );

function myplugin_register_ability_categories(): void {
    wp_register_ability_category(
        'myplugin-site-information',
        array(
            'label'       => __( 'Site Information', 'myplugin' ),
            'description' => __( 'Abilities that report on site state.', 'myplugin' ),
        )
    );
}

If two plugins register the same category slug, the second registration fails with _doing_it_wrong() and returns null; the first category remains registered. Pick a slug specific enough to avoid collisions (myplugin-tools, not tools).

Registering an ability — minimal example

add_action( 'wp_abilities_api_init', 'myplugin_register_abilities' );

function myplugin_register_abilities(): void {
    wp_register_ability(
        'myplugin/site-info',
        array(
            'label'       => __( 'Site Info', 'myplugin' ),
            'description' => __( 'Returns information about this WordPress site.', 'myplugin' ),
            'category'    => 'myplugin-site-information',
            'input_schema'  => array(),
            'output_schema' => array(
                'type'       => 'object',
                'properties' => array(
                    'site_name' => array(
                        'type'        => 'string',
                        'description' => __( 'The name of the site.', 'myplugin' ),
                    ),
                    'php_version' => array(
                        'type'        => 'string',
                        'description' => __( 'PHP version running on the server.', 'myplugin' ),
                    ),
                ),
            ),
            'execute_callback'    => 'myplugin_get_site_info',
            'permission_callback' => static fn () => current_user_can( 'manage_options' ),
            'meta' => array(
                'public' => true,
            ),
        )
    );
}

function myplugin_get_site_info(): array {
    return array(
        'site_name'   => get_bloginfo( 'name' ),
        'php_version' => PHP_VERSION,
    );
}

Registration arguments — what's required

WordPress 6.9+ validates these arguments in WP_Ability::prepare_properties() and WP_Abilities_Registry::register():

Field Required Type Notes
label yes string Translated, human-readable.
description yes string Translated. AI agents read this to decide whether to invoke.
category yes string Slug must match a registered category.
input_schema no array Required in practice when the ability accepts input. If omitted/empty and input is provided, execution returns ability_missing_input_schema.
output_schema no in core, yes in docs/review array Core allows omission and then skips output validation. Provide it anyway for contracts, REST discovery, and agents.
execute_callback yes callable The PHP function that runs.
permission_callback yes callable Returns true, false, or WP_Error. Enforced by WP_Ability::execute() and by REST run permission checks.
meta optional array Annotations and exposure flags. In 7.1, public seeds channel defaults; explicit show_in_rest wins for REST.

The description is the most important field for AI consumers — it's what an agent reads to decide whether the Ability is the right tool. Write it the way you'd describe the function to a smart colleague reviewing the API.

Callback arity depends on input_schema: if the schema is empty, WordPress calls execute_callback and permission_callback with no arguments. If the schema is non-empty, WordPress passes the normalized input as the first argument.

Naming convention

Ability identifier: namespace/ability-name (slash-separated, kebab-case on both sides).

  • The namespace is your plugin slug or a sub-system inside it (myplugin, myplugin-billing).
  • The ability-name describes what it does (get-site-info, cancel-subscription, summarize-post).

Pick names a non-developer could read aloud — myplugin/cancel-subscription is better than myplugin/cancel-sub-v2. Treat the identifier as a public API contract: once an Ability is shipped and an AI agent or external client depends on it, renaming is a breaking change.

Server-side PHP registration accepts exactly two slash-separated segments (myplugin/do-thing). The client-side @wordpress/abilities registry accepts 2-4 segments, but use the two-segment form for abilities that need to round-trip through PHP, REST, or the WP AI Client.

Permission callback

Required. Return true to allow execution, false to deny, or WP_Error for a structured REST permission failure. WP_Ability::execute() enforces this for direct PHP execution; if the callback returns WP_Error there, core logs it via _doing_it_wrong() and returns a generic ability_invalid_permissions error so the message is not leaked. The REST run endpoint also checks permissions before execution:

'permission_callback' => static function (): bool {
    return current_user_can( 'manage_options' );
},

For object-level abilities (operate on a specific post / user / order), pass the object ID through the meta-capability check:

'permission_callback' => static function ( array $input ): bool {
    return current_user_can( 'edit_post', (int) ( $input['post_id'] ?? 0 ) );
},

Keep normalization, validation, and permission filters/callbacks cheap, deterministic, and side-effect-free. In the REST run flow, WordPress can normalize, validate, and check permissions before dispatch, then WP_Ability::execute() repeats the execution pipeline. Do not put billing, remote calls, writes, or one-shot state in these stages.

Executing an ability from PHP

$ability = wp_get_ability( 'myplugin/site-info' );
if ( $ability instanceof WP_Ability ) {
    $result = $ability->execute(); // This ability has no input_schema.
}

For an ability with a non-empty input_schema, pass the matching normalized input array to execute( $input ).

Public lookup helpers:

  • wp_get_ability( string $id ): ?WP_Ability
  • wp_get_abilities( array $args = array() ): array<string, WP_Ability>
  • wp_has_ability( string $id ): bool

For inspection / debugging during development, use wp shell:

$ wp shell
wp> wp_has_ability( 'myplugin/site-info' );
=> bool(true)
wp> wp_get_ability( 'myplugin/site-info' );
=> object(WP_Ability) ...

REST and JavaScript exposure

On WordPress 7.1, set meta.public = true when the operation is intended for client channels. It defaults show_in_rest to true; an explicit show_in_rest => false overrides it. Set only show_in_rest => true when REST exposure is intended but broader public-channel intent is not. Neither flag grants execution permission.

Core exposes list/get/run routes, enforces the ability permission callback, selects GET/POST/DELETE from annotations, and prepares schemas with wp_prepare_json_schema_for_client(). List requests support bounded pagination, category, namespace, and declared meta filters. Public constraints belong in JSON Schema; PHP callbacks and WordPress-only keywords are removed from client schemas. Read reference.md for route, lifecycle, and filtering details.

Use @wordpress/core-abilities to bridge server abilities into JavaScript and await its exported ready promise before immediate execution. Use @wordpress/abilities alone for client-only categories/abilities. Client-side permission callbacks never replace PHP authorization. Read reference.md for the verified route shapes, module bootstrap, and complete client examples.

WP AI Client integration

WordPress 7.0+ can expose selected server-side abilities as AI model function declarations:

$result = wp_ai_client_prompt( 'Summarize current site diagnostics.' )
    ->using_abilities( 'core/get-site-info', 'core/get-environment-info' )
    ->generate_text_result();

For AI-driven ability execution, instantiate WP_AI_Client_Ability_Function_Resolver with an explicit allowlist. Never give a model a blanket list of all abilities; the ability's own permission_callback still runs, but the resolver allowlist is the first boundary.

MCP adapter — bridging to AI agents

The WordPress team maintains a separate MCP adapter that can bridge eligible Abilities to the Model Context Protocol. Exposure is adapter configuration and metadata dependent; do not assume every registered or merely public Ability becomes an MCP tool. The ability's permission callback remains mandatory.

Architectural details of the MCP adapter were not included in the Nov 2025 announcement (deferred to a follow-up post). For now, the practical takeaway: registering a clean, well-described Ability also makes it AI-agent-ready without any extra code on your side, IF the site administrator installs the MCP adapter.

When to use an Ability vs a custom REST route vs a custom hook

Situation Use
Operation that AI agents / external tools should discover and invoke Ability
Typed operation called from your own JS and potentially reusable by tools Ability
Resource CRUD/public headless API with stable HTTP semantics Custom REST route/controller
Internal extension point (other plugins / themes can wire callbacks) Custom action / filter hook (see wp-plugin-hooks)
Pure UI rendering with no logic to expose externally Block / shortcode, NOT an Ability
Webhook receiver from a third-party service Custom REST route — webhooks usually need a fixed URL contract that doesn't fit the Abilities namespace shape

Choose by contract, not novelty: Ability for a discoverable typed operation, REST for an HTTP resource/integration contract, and an action/filter for an in-process extension point.

Critical rules

  • Register on wp_abilities_api_init for abilities, wp_abilities_api_categories_init for categories. Don't register on init directly.
  • Categories are prerequisite. Register categories before abilities.
  • Identifier namespace/ability-name is a public contract — treat it like a versioned API.
  • Always provide a permission_callback; core requires it. Use object-aware capability checks for object-scoped abilities.
  • Provide schemas even when core does not force them. If the ability accepts input, input_schema is required in practice; without it, provided input is rejected. output_schema is essential for documentation and agents.
  • Use meta.public and channel flags deliberately on 7.1. public seeds show_in_rest; explicit show_in_rest wins. Exposure never replaces authorization.
  • Use @wordpress/core-abilities for server abilities in JS. Use @wordpress/abilities alone only for client-only abilities.
  • Prepare schemas before sending them to non-WordPress clients. Use wp_prepare_json_schema_for_client(); allowing a keyword does not add server validation for it.
  • Treat 7.1 lifecycle filters as trusted policy code. wp_pre_execute_ability bypasses normalization, validation, permission, callback, output validation, and before/after actions when it short-circuits.
  • Allowlist abilities for WP AI Client tool use. Do not pass all registered abilities to a prompt.
  • Description is the AI's tool selector. Write it for a reader who doesn't know your plugin.
  • For pre-6.9 sites, use the Composer package or feature plugin. Don't reimplement the registry yourself.

Common mistakes

Audit wrong lifecycle hooks, missing categories/schemas/permissions, unconditional permission on privileged writes, empty descriptions, generic namespaces, client categories registered after their abilities, and blanket AI tool allowlists. Corrected examples are in reference.md.

Cross-references

  • Run wp-rest-api when comparing against custom register_rest_route patterns. Many existing custom REST endpoints in plugin codebases would be cleaner as Abilities.
  • Run wp-ai-client when abilities are being exposed as AI model tools or chained through prompt workflows.
  • Run wp-plugin-hooks for the case where the right primitive is actually an apply_filters extension point inside the plugin, not a publicly invocable Ability.
  • Run wp-security-audit on the execute_callback — it's a request-handling endpoint reached through REST and MCP. Sanitize / validate inputs, escape outputs, treat the input as untrusted regardless of the schema.

What this skill does NOT cover

  • The MCP adapter's internal architecture and configuration (deferred to a separate post in the official series).
  • Advanced @wordpress/abilities store internals beyond registration, querying, execution, and unregistering.
  • Multisite-network considerations for Abilities (not addressed in the Nov 2025 announcement; verify before relying on per-site vs network registration semantics).
  • WP-CLI command coverage beyond wp shell inspection (no dedicated CLI commands documented yet).
  • Building a full hook extension layer around abilities. The verified WordPress 7.1 execution lifecycle is summarized in reference.md.

References

Files (wp-agent-skills)
  • reference.md 8.1 KB
    # Abilities API REST and JavaScript Reference
    
    Read this after the main skill when implementing REST discovery or client-side
    abilities on WordPress 7.1.
    
    ## REST routes
    
    With effective `meta.show_in_rest = true`, core exposes:
    
    - `GET /wp-json/wp-abilities/v1/abilities`
    - `GET /wp-json/wp-abilities/v1/abilities/{namespace}/{ability}`
    - `GET|POST|DELETE /wp-json/wp-abilities/v1/abilities/{namespace}/{ability}/run`
    - `GET /wp-json/wp-abilities/v1/categories`
    - `GET /wp-json/wp-abilities/v1/categories/{slug}`
    
    The run controller expects GET for `readonly: true`, DELETE for destructive and
    idempotent abilities, and POST otherwise. Wrong methods return 405. List/get
    require an authenticated user with `read`; run also enforces the ability's
    permission callback. Inspect `WP_REST_Abilities_V1_*_Controller` on the target
    core version if a package or handbook disagrees.
    
    REST responses omit internal schema keys such as `sanitize_callback`,
    `validate_callback`, and `arg_options`. Express public constraints using JSON
    Schema keywords.
    
    In WordPress 7.1 the collection route supports `page` (minimum 1), `per_page`
    (1-100, default 50), `category`, `namespace`, and `meta`. It always forces the
    internal `show_in_rest => true` match. It sends `X-WP-Total`,
    `X-WP-TotalPages`, and pagination links; `HEAD` returns an empty body with the
    same collection headers. Use `rest_abilities_collection_params` to declare the
    schema for custom meta query fields before expecting query-string coercion.
    
    The 7.1 run controller coerces already-valid GET/DELETE query input into the
    ability's schema types. It deliberately leaves invalid input unchanged so
    validation rejects it instead of sanitization silently making it valid. POST
    reads `input` from the JSON body; GET/DELETE read it from the query string.
    
    ## Public exposure flags
    
    ```php
    'meta' => array(
        'public'       => true,
        'show_in_rest' => false,
    ),
    ```
    
    WordPress 7.1 resolves REST exposure in this order:
    
    1. explicit `show_in_rest`;
    2. `public` as its default;
    3. false.
    
    `public` communicates broad client-facing intent for REST, MCP, AI, and future
    channels; it is not authorization and does not force every adapter to expose
    the ability. An explicit channel flag can narrow or broaden one channel.
    
    ## Filtered server discovery
    
    WordPress 7.1 accepts filters in `wp_get_abilities()`:
    
    ```php
    $abilities = wp_get_abilities(
        array(
            'category'  => 'myplugin-actions',
            'namespace' => 'myplugin',
            'meta'      => array(
                'public'      => true,
                'annotations' => array( 'readonly' => true ),
            ),
            'item_include_callback' => static fn ( WP_Ability $ability ): bool =>
                current_user_can( 'read' ),
            'result_callback' => static fn ( array $items ): array =>
                array_slice( $items, 0, 20, true ),
        )
    );
    ```
    
    Category, namespace, and meta use AND logic. Nested meta arrays are matched
    recursively and scalar values strictly. Results stay keyed by ability name
    unless a callback reshapes them. The pipeline order is declarative filters,
    caller item callback, `wp_get_abilities_item_include`, caller result callback,
    then `wp_get_abilities_result`. Ecosystem filters fire even for a no-argument
    call. Use the registry's raw getter only when deliberately bypassing all of
    those policies.
    
    ## WordPress 7.1 execution lifecycle
    
    The exact `WP_Ability::execute()` order is:
    
    1. `wp_ability_invoked` receives raw input for every attempt.
    2. `wp_pre_execute_ability` may short-circuit everything below.
    3. schema default normalization, then `wp_ability_normalize_input`.
    4. schema validation, then `wp_ability_validate_input`.
    5. permission callback, then `wp_ability_permission_result`.
    6. `wp_before_execute_ability`.
    7. execute callback, then `wp_ability_execute_result`.
    8. schema validation, then `wp_ability_validate_output`.
    9. `wp_after_execute_ability`.
    
    `wp_pre_execute_ability` receives a unique `WP_Filter_Sentinel`. Return that
    exact value to continue; any different value—including `null` or `false`—is a
    final result and bypasses input/output validation and permission checks. Use it
    only in trusted server code, and enforce equivalent authorization and result
    integrity when caching or mocking.
    
    Normalization can return `WP_Error`. Validation filters receive `true` or the
    core `WP_Error`; return `false` for a generic invalid error or a non-empty
    `WP_Error` for a specific failure. Permission filtering coerces unexpected
    types to denial. The result filter can recover from an execution `WP_Error` or
    convert success into an error. Before/after actions now receive the
    `WP_Ability` object; old callbacks must keep their accepted-argument count
    compatible.
    
    REST dispatch runs normalization/validation/permission before the callback and
    `execute()` runs them again. Those stages must therefore be deterministic,
    read-only, and safe to repeat.
    
    ## Client-safe JSON Schema
    
    Use `wp_prepare_json_schema_for_client( $schema )` before passing a
    WordPress-authored schema to browsers or AI providers. The default `draft-04`
    profile preserves a broader draft-04 vocabulary; pass `rest-api` for the
    historical REST subset. It recursively strips unapproved/PHP callback keys,
    converts per-property `required: true` to the parent draft-04 `required` array,
    and emits an empty object default as an object instead of an empty JSON array.
    
    `wp_get_json_schema_allowed_keywords()` can be filtered, but exposing a keyword
    does not make WordPress validate or sanitize it. Keep the server contract within
    the validator's supported subset.
    
    ## Server abilities in JavaScript
    
    ```php
    add_action( 'admin_enqueue_scripts', static function ( string $hook_suffix ): void {
        if ( 'settings_page_myplugin' !== $hook_suffix ) {
            return;
        }
        wp_enqueue_script_module( '@wordpress/core-abilities' );
    } );
    ```
    
    ```js
    const { ready } = await import( '@wordpress/core-abilities' );
    const { executeAbility } = await import( '@wordpress/abilities' );
    
    await ready;
    const result = await executeAbility( 'myplugin/site-info' );
    ```
    
    ## Client-only abilities
    
    Enqueue `@wordpress/abilities`, import functions from the module rather than a
    global, and register the category first.
    
    ```js
    const {
        registerAbility,
        registerAbilityCategory,
        executeAbility,
    } = await import( '@wordpress/abilities' );
    
    registerAbilityCategory( 'myplugin-actions', {
        label: 'My Plugin Actions',
        description: 'Actions provided by My Plugin.',
    } );
    
    registerAbility( {
        name: 'myplugin/navigate-to-settings',
        label: 'Navigate to Settings',
        description: 'Navigates to the plugin settings screen.',
        category: 'myplugin-actions',
        permissionCallback: () => true,
        callback: async () => {
            window.location.href = '/wp-admin/options-general.php?page=myplugin';
            return { success: true };
        },
        output_schema: {
            type: 'object',
            properties: { success: { type: 'boolean' } },
            required: [ 'success' ],
        },
    } );
    ```
    
    `executeAbility()` validates input and output. A client permission callback
    only governs local execution; server abilities still require PHP authorization.
    
    ## Correct common failures
    
    ```php
    // Register on the API hook, after its category exists.
    add_action( 'wp_abilities_api_init', static function (): void {
        wp_register_ability( 'myplugin/do-thing', array(
            'label'               => __( 'Do thing', 'myplugin' ),
            'description'         => __( 'Runs a defined plugin operation.', 'myplugin' ),
            'category'            => 'myplugin-actions',
            'input_schema'        => array(
                'type'       => 'object',
                'properties' => array( 'post_id' => array( 'type' => 'integer' ) ),
                'required'   => array( 'post_id' ),
            ),
            'output_schema'       => array( 'type' => 'object' ),
            'execute_callback'    => 'myplugin_do_thing',
            'permission_callback' => static function ( array $input ): bool {
                return current_user_can( 'edit_post', $input['post_id'] );
            },
        ) );
    } );
    ```
    
    Avoid `init`, unregistered categories, empty descriptions, missing schemas,
    generic namespaces such as `tools/*`, unconditional access to privileged
    writes, and exposing every registered ability to an AI resolver.
    
  • SKILL.md 18.1 KB
    ---
    name: wp-abilities-api
    description: >-
      Register WordPress Abilities: machine-readable plugin
      operations with JSON Schema contracts, required permission callbacks,
      optional REST exposure, client-side abilities, and AI/MCP-friendly
      discovery. Covers categories, wp_register_ability, WP_Ability::execute,
      REST run endpoints, @wordpress/abilities, @wordpress/core-abilities,
      meta.public/show_in_rest, filtered discovery, execution lifecycle hooks,
      client-safe schemas, annotations, and Ability vs REST route vs custom hook
      decisions. Use when exposing plugin functionality to agents, admin JS,
      external tools, WP AI Client workflows, or reviewing AI integration code.
    metadata:
      wp-skills-author: "Soczó Kristóf"
      wp-skills-contact: "mailto:lonsdale201@hotmail.com"
      wp-skills-plugin: "wordpress"
      wp-skills-plugin-version-tested: "6.9 - 7.1"
      wp-skills-wp-version-tested: "7.1"
      wp-skills-php-min: "7.4"
      wp-skills-last-updated: "2026-08-20"
    ---
    
    # WordPress Abilities API
    
    A standardized registry for plugin / theme / core functionality, designed primarily so AI agents and external tools can **discover** what a WordPress site can do and **invoke** those capabilities through a uniform contract. Each Ability has a stable identifier, JSON-Schema-typed inputs and outputs, and a required permission callback. `WP_Ability::execute()` validates input, checks permissions, executes, and validates output.
    
    Pre-Abilities, the same functionality was scattered across `do_action`, `apply_filters`, custom REST routes, public PHP functions, and ad-hoc plugin APIs (each with its own conventions). The Abilities API consolidates that into one machine-readable surface.
    
    This skill is grounded in WordPress 7.1 core behavior. Server-side Abilities shipped in WordPress 6.9; WordPress 7.0 added the client packages and WP AI Client integration. WordPress 7.1 adds unified public exposure metadata, filtered discovery, a complete execution-filter lifecycle, client-safe JSON Schema preparation, REST collection filters/pagination, and schema-aware run-input coercion. Verify version-sensitive behavior against `wp-includes/abilities-api`, the `WP_REST_Abilities_V1_*` controllers, and the abilities script modules.
    
    ## When to use this skill
    
    Trigger when ANY of the following is true:
    
    - Scaffolding plugin functionality that AI agents (Claude, ChatGPT, custom MCP clients) should be able to discover and invoke.
    - Designing an admin tool whose logic should be reachable from BOTH PHP code and the block editor JS without writing two parallel implementations.
    - Reviewing a plugin where you see `wp_register_ability`, `wp_get_ability`, `WP_Ability`, the `wp_abilities_api_init` action, or the `@wordpress/abilities` JS package.
    - Registering client-side admin operations with `@wordpress/abilities`, or loading server abilities into JS with `@wordpress/core-abilities`.
    - Passing abilities to `wp_ai_client_prompt()->using_abilities()` or handling `WP_AI_Client_Ability_Function_Resolver`.
    - Deciding whether to expose a feature as a custom REST endpoint vs an Ability.
    - Migrating an existing custom REST endpoint to the Abilities API.
    
    ## Availability and installation
    
    Three installation paths, depending on WP version:
    
    1. **WordPress 7.0+** - server-side PHP API plus client-side `@wordpress/abilities` and `@wordpress/core-abilities` script modules ship in core. Feature-detect 7.1 additions before using them on 7.0.
    2. **WordPress 6.9.x** - server-side PHP API, registry, REST exposure, and core integration ship in core. Feature-detect the JS packages before relying on them.
    3. **WordPress < 6.9 - Composer package** for plugins that bundle their own dependencies:
       ```bash
       composer require wordpress/abilities-api
       ```
       Package: <https://packagist.org/packages/wordpress/abilities-api>.
    4. **WordPress < 6.9 - feature plugin** for site-wide install: download the latest release ZIP from <https://github.com/WordPress/abilities-api> or install via WP admin.
    
    For JavaScript, prefer script modules and explicit dependencies. Do not assume a `window.wp.abilities` global exists.
    
    ## Registering a category (prerequisite)
    
    Every Ability MUST belong to a category. Register categories on the dedicated init action ([Abilities API docs](https://developer.wordpress.org/apis/abilities-api/)):
    
    ```php
    add_action( 'wp_abilities_api_categories_init', 'myplugin_register_ability_categories' );
    
    function myplugin_register_ability_categories(): void {
        wp_register_ability_category(
            'myplugin-site-information',
            array(
                'label'       => __( 'Site Information', 'myplugin' ),
                'description' => __( 'Abilities that report on site state.', 'myplugin' ),
            )
        );
    }
    ```
    
    If two plugins register the same category slug, the second registration fails with `_doing_it_wrong()` and returns `null`; the first category remains registered. Pick a slug specific enough to avoid collisions (`myplugin-tools`, not `tools`).
    
    ## Registering an ability — minimal example
    
    ```php
    add_action( 'wp_abilities_api_init', 'myplugin_register_abilities' );
    
    function myplugin_register_abilities(): void {
        wp_register_ability(
            'myplugin/site-info',
            array(
                'label'       => __( 'Site Info', 'myplugin' ),
                'description' => __( 'Returns information about this WordPress site.', 'myplugin' ),
                'category'    => 'myplugin-site-information',
                'input_schema'  => array(),
                'output_schema' => array(
                    'type'       => 'object',
                    'properties' => array(
                        'site_name' => array(
                            'type'        => 'string',
                            'description' => __( 'The name of the site.', 'myplugin' ),
                        ),
                        'php_version' => array(
                            'type'        => 'string',
                            'description' => __( 'PHP version running on the server.', 'myplugin' ),
                        ),
                    ),
                ),
                'execute_callback'    => 'myplugin_get_site_info',
                'permission_callback' => static fn () => current_user_can( 'manage_options' ),
                'meta' => array(
                    'public' => true,
                ),
            )
        );
    }
    
    function myplugin_get_site_info(): array {
        return array(
            'site_name'   => get_bloginfo( 'name' ),
            'php_version' => PHP_VERSION,
        );
    }
    ```
    
    ## Registration arguments — what's required
    
    WordPress 6.9+ validates these arguments in `WP_Ability::prepare_properties()` and `WP_Abilities_Registry::register()`:
    
    | Field | Required | Type | Notes |
    |---|---|---|---|
    | `label` | yes | string | Translated, human-readable. |
    | `description` | yes | string | Translated. AI agents read this to decide whether to invoke. |
    | `category` | yes | string | Slug must match a registered category. |
    | `input_schema` | no | array | Required in practice when the ability accepts input. If omitted/empty and input is provided, execution returns `ability_missing_input_schema`. |
    | `output_schema` | no in core, yes in docs/review | array | Core allows omission and then skips output validation. Provide it anyway for contracts, REST discovery, and agents. |
    | `execute_callback` | yes | callable | The PHP function that runs. |
    | `permission_callback` | yes | callable | Returns `true`, `false`, or `WP_Error`. Enforced by `WP_Ability::execute()` and by REST run permission checks. |
    | `meta` | optional | array | Annotations and exposure flags. In 7.1, `public` seeds channel defaults; explicit `show_in_rest` wins for REST. |
    
    The `description` is the most important field for AI consumers — it's what an agent reads to decide whether the Ability is the right tool. Write it the way you'd describe the function to a smart colleague reviewing the API.
    
    Callback arity depends on `input_schema`: if the schema is empty, WordPress calls `execute_callback` and `permission_callback` with no arguments. If the schema is non-empty, WordPress passes the normalized input as the first argument.
    
    ## Naming convention
    
    Ability identifier: `namespace/ability-name` (slash-separated, kebab-case on both sides).
    
    - The `namespace` is your plugin slug or a sub-system inside it (`myplugin`, `myplugin-billing`).
    - The `ability-name` describes what it does (`get-site-info`, `cancel-subscription`, `summarize-post`).
    
    Pick names a non-developer could read aloud — `myplugin/cancel-subscription` is better than `myplugin/cancel-sub-v2`. Treat the identifier as a public API contract: once an Ability is shipped and an AI agent or external client depends on it, renaming is a breaking change.
    
    Server-side PHP registration accepts exactly two slash-separated segments (`myplugin/do-thing`). The client-side `@wordpress/abilities` registry accepts 2-4 segments, but use the two-segment form for abilities that need to round-trip through PHP, REST, or the WP AI Client.
    
    ## Permission callback
    
    Required. Return `true` to allow execution, `false` to deny, or `WP_Error` for a structured REST permission failure. `WP_Ability::execute()` enforces this for direct PHP execution; if the callback returns `WP_Error` there, core logs it via `_doing_it_wrong()` and returns a generic `ability_invalid_permissions` error so the message is not leaked. The REST run endpoint also checks permissions before execution:
    
    ```php
    'permission_callback' => static function (): bool {
        return current_user_can( 'manage_options' );
    },
    ```
    
    For object-level abilities (operate on a specific post / user / order), pass the object ID through the meta-capability check:
    
    ```php
    'permission_callback' => static function ( array $input ): bool {
        return current_user_can( 'edit_post', (int) ( $input['post_id'] ?? 0 ) );
    },
    ```
    
    Keep normalization, validation, and permission filters/callbacks cheap, deterministic, and side-effect-free. In the REST run flow, WordPress can normalize, validate, and check permissions before dispatch, then `WP_Ability::execute()` repeats the execution pipeline. Do not put billing, remote calls, writes, or one-shot state in these stages.
    
    ## Executing an ability from PHP
    
    ```php
    $ability = wp_get_ability( 'myplugin/site-info' );
    if ( $ability instanceof WP_Ability ) {
        $result = $ability->execute(); // This ability has no input_schema.
    }
    ```
    
    For an ability with a non-empty `input_schema`, pass the matching normalized
    input array to `execute( $input )`.
    
    Public lookup helpers:
    
    - `wp_get_ability( string $id ): ?WP_Ability`
    - `wp_get_abilities( array $args = array() ): array<string, WP_Ability>`
    - `wp_has_ability( string $id ): bool`
    
    For inspection / debugging during development, use `wp shell`:
    
    ```
    $ wp shell
    wp> wp_has_ability( 'myplugin/site-info' );
    => bool(true)
    wp> wp_get_ability( 'myplugin/site-info' );
    => object(WP_Ability) ...
    ```
    
    ## REST and JavaScript exposure
    
    On WordPress 7.1, set `meta.public = true` when the operation is intended for
    client channels. It defaults `show_in_rest` to true; an explicit
    `show_in_rest => false` overrides it. Set only `show_in_rest => true` when REST
    exposure is intended but broader public-channel intent is not. Neither flag
    grants execution permission.
    
    Core exposes list/get/run routes, enforces the ability permission callback,
    selects GET/POST/DELETE from annotations, and prepares schemas with
    `wp_prepare_json_schema_for_client()`. List requests support bounded pagination,
    category, namespace, and declared meta filters. Public constraints belong in
    JSON Schema; PHP callbacks and WordPress-only keywords are removed from client
    schemas. Read `reference.md` for route, lifecycle, and filtering details.
    
    Use `@wordpress/core-abilities` to bridge server abilities into JavaScript and
    await its exported `ready` promise before immediate execution. Use
    `@wordpress/abilities` alone for client-only categories/abilities. Client-side
    permission callbacks never replace PHP authorization. Read `reference.md` for
    the verified route shapes, module bootstrap, and complete client examples.
    
    ## WP AI Client integration
    
    WordPress 7.0+ can expose selected server-side abilities as AI model function declarations:
    
    ```php
    $result = wp_ai_client_prompt( 'Summarize current site diagnostics.' )
        ->using_abilities( 'core/get-site-info', 'core/get-environment-info' )
        ->generate_text_result();
    ```
    
    For AI-driven ability execution, instantiate `WP_AI_Client_Ability_Function_Resolver` with an explicit allowlist. Never give a model a blanket list of all abilities; the ability's own `permission_callback` still runs, but the resolver allowlist is the first boundary.
    
    ## MCP adapter — bridging to AI agents
    
    The WordPress team maintains a separate **MCP adapter** that can bridge eligible Abilities to the [Model Context Protocol](https://modelcontextprotocol.io/). Exposure is adapter configuration and metadata dependent; do not assume every registered or merely `public` Ability becomes an MCP tool. The ability's permission callback remains mandatory.
    
    Architectural details of the MCP adapter were not included in the Nov 2025 announcement (deferred to a follow-up post). For now, the practical takeaway: registering a clean, well-described Ability also makes it AI-agent-ready without any extra code on your side, IF the site administrator installs the MCP adapter.
    
    ## When to use an Ability vs a custom REST route vs a custom hook
    
    | Situation | Use |
    |---|---|
    | Operation that AI agents / external tools should discover and invoke | **Ability** |
    | Typed operation called from your own JS and potentially reusable by tools | Ability |
    | Resource CRUD/public headless API with stable HTTP semantics | Custom REST route/controller |
    | Internal extension point (other plugins / themes can wire callbacks) | Custom action / filter hook (see `wp-plugin-hooks`) |
    | Pure UI rendering with no logic to expose externally | Block / shortcode, NOT an Ability |
    | Webhook receiver from a third-party service | Custom REST route — webhooks usually need a fixed URL contract that doesn't fit the Abilities namespace shape |
    
    Choose by contract, not novelty: Ability for a discoverable typed operation,
    REST for an HTTP resource/integration contract, and an action/filter for an
    in-process extension point.
    
    ## Critical rules
    
    - **Register on `wp_abilities_api_init`** for abilities, `wp_abilities_api_categories_init` for categories. Don't register on `init` directly.
    - **Categories are prerequisite.** Register categories before abilities.
    - **Identifier `namespace/ability-name` is a public contract** — treat it like a versioned API.
    - **Always provide a `permission_callback`**; core requires it. Use object-aware capability checks for object-scoped abilities.
    - **Provide schemas even when core does not force them.** If the ability accepts input, `input_schema` is required in practice; without it, provided input is rejected. `output_schema` is essential for documentation and agents.
    - **Use `meta.public` and channel flags deliberately on 7.1.** `public` seeds `show_in_rest`; explicit `show_in_rest` wins. Exposure never replaces authorization.
    - **Use `@wordpress/core-abilities` for server abilities in JS.** Use `@wordpress/abilities` alone only for client-only abilities.
    - **Prepare schemas before sending them to non-WordPress clients.** Use `wp_prepare_json_schema_for_client()`; allowing a keyword does not add server validation for it.
    - **Treat 7.1 lifecycle filters as trusted policy code.** `wp_pre_execute_ability` bypasses normalization, validation, permission, callback, output validation, and before/after actions when it short-circuits.
    - **Allowlist abilities for WP AI Client tool use.** Do not pass all registered abilities to a prompt.
    - **Description is the AI's tool selector.** Write it for a reader who doesn't know your plugin.
    - **For pre-6.9 sites, use the Composer package or feature plugin.** Don't reimplement the registry yourself.
    
    ## Common mistakes
    
    Audit wrong lifecycle hooks, missing categories/schemas/permissions,
    unconditional permission on privileged writes, empty descriptions, generic
    namespaces, client categories registered after their abilities, and blanket AI
    tool allowlists. Corrected examples are in `reference.md`.
    
    ## Cross-references
    
    - Run **`wp-rest-api`** when comparing against custom `register_rest_route` patterns. Many existing custom REST endpoints in plugin codebases would be cleaner as Abilities.
    - Run **`wp-ai-client`** when abilities are being exposed as AI model tools or chained through prompt workflows.
    - Run **`wp-plugin-hooks`** for the case where the right primitive is actually an `apply_filters` extension point inside the plugin, not a publicly invocable Ability.
    - Run **`wp-security-audit`** on the `execute_callback` — it's a request-handling endpoint reached through REST and MCP. Sanitize / validate inputs, escape outputs, treat the input as untrusted regardless of the schema.
    
    ## What this skill does NOT cover
    
    - The MCP adapter's internal architecture and configuration (deferred to a separate post in the official series).
    - Advanced `@wordpress/abilities` store internals beyond registration, querying, execution, and unregistering.
    - Multisite-network considerations for Abilities (not addressed in the Nov 2025 announcement; verify before relying on per-site vs network registration semantics).
    - WP-CLI command coverage beyond `wp shell` inspection (no dedicated CLI commands documented yet).
    - Building a full hook extension layer around abilities. The verified WordPress 7.1 execution lifecycle is summarized in `reference.md`.
    
    ## References
    
    - [Abilities API documentation](https://developer.wordpress.org/apis/abilities-api/) — primary authoritative source.
    - [Introducing the Abilities API (Nov 2025)](https://developer.wordpress.org/news/2025/11/introducing-the-wordpress-abilities-api/) — announcement post with concrete examples.
    - [Client-Side Abilities API in WordPress 7.0](https://make.wordpress.org/core/2026/03/24/client-side-abilities-api-in-wordpress-7-0/).
    - [Abilities API improvements in WordPress 7.1](https://make.wordpress.org/core/2026/07/31/abilities-api-improvements-in-wordpress-7-1/).
    - [`wordpress/abilities-api` Composer package](https://packagist.org/packages/wordpress/abilities-api).
    - [WordPress/abilities-api on GitHub](https://github.com/WordPress/abilities-api) — active feature-plugin repository (the older archived repo is NOT canonical).
    - [Make WordPress Core AI team blog](https://make.wordpress.org/ai/) — ongoing development discussion.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related