WordPress 6.9 and the Abilities API: A Detailed Technical Breakdown with Real Examples

WordPress 6.9 introduces the Abilities API, a new foundational system that standardizes how WordPress core, plugins, and themes define, describe, and expose their functionalities. Instead of relying on scattered approaches such as hooks, functions, or custom REST routes, developers can now register abilities with structured schemas, permissions, and unified execution paths accessible from PHP, JavaScript, and REST API. This article provides an in-depth explanation of the Abilities API using the real-world example of the List All URLs plugin.

WordPress 6.9 and the Abilities API: A Detailed Technical Breakdown with Real Examples

What is the Abilities API in WordPress 6.9?

The Abilities API is a system that allows you to describe each WordPress feature as an “ability” that includes:

  • A unique identifier (namespace/ability).
  • Human-readable descriptions (label, description).
  • Input and output schemas defined using JSON Schema arrays.
  • An execution handler (execute_callback).
  • A permission handler (permission_callback).
  • Optional exposure through the REST API and other client layers.

Instead of each plugin creating its own functions, hooks, and REST routes, the Abilities API provides a centralized registry where abilities are declared and can be discovered, inspected, and executed in a unified way.

View the original article at: Introducing the WordPress Abilities API.

The previous problem: too many APIs, no shared standard

Before the Abilities API, a plugin or theme could expose its features in many different ways:

  • Global PHP functions or class methods.
  • Action/filter hooks for other developers to attach to.
  • Custom REST API routes created with register_rest_route.
  • Blocks interacting directly via JavaScript.

The problem was that there was no enforced standard describing what input a feature accepts, what output it returns, or what permissions are required. The Abilities API solves this by moving everything into a unified, human-readable and machine-readable model.

Foundation example: the List All URLs plugin before Abilities

The List All URLs plugin is a simple example: it adds a page under Tools that lets administrators select content types (post, page, custom post type) and display their URLs. Before using the Abilities API, this plugin had a single core function handling all data retrieval:

<?php
/**
 * Generate a list of URLs based on the provided arguments.
 * Optionally make them clickable links.
 *
 * @param array $arguments Arguments to customize the URL generation.
 * @param bool  $makelinks Whether to return clickable links or plain URLs (escaped).
 *
 * @return array List of generated URLs.
 */
function list_all_urls_generate_url_list( array $arguments = array(), bool $makelinks = false ): array {
    $default_args = array(
        'post_type'      => 'post',
        'posts_per_page' => -1,
        'post_status'    => 'publish',
    );

    $args  = wp_parse_args( $arguments, $default_args );
    $posts = get_posts( $args );

    $links = array();

    foreach ( $posts as $post ) {
        $permalink = get_permalink( $post );

        if ( $makelinks ) {
            $links[] = '<a href="' . esc_url( $permalink ) . '">' . esc_html( $permalink ) . '</a>';
        } else {
            $links[] = esc_html( $permalink );
        }
    }

    return $links;
}

This function does three things:

  1. Merges input arguments with defaults (post_type, posts_per_page, post_status).
  2. Uses get_posts to fetch the corresponding posts.
  3. Formats the output as plain URLs or HTML anchor tags depending on the makelinks option.

This is the core business logic that other layers would reuse (admin page, REST, block, etc.).

Traditional implementation with REST API and Block Editor

To allow external systems to access the list of URLs and display them inside the Block Editor, the traditional approach was to:

  • Register a new REST route.
  • Write a callback for that route, calling list_all_urls_generate_url_list.
  • Create a block that uses apiFetch to call the REST endpoint and render the list inside the Editor.
  • If you want dynamic rendering on the frontend, use a dynamic block with a render.php file.

A minimal REST route example:

<?php
add_action( 'rest_api_init', 'list_all_urls_register_rest_route' );

function list_all_urls_register_rest_route(): void {
    register_rest_route(
        'list-all-urls/v1',
        '/urls',
        array(
            'methods'  => 'GET',
            'callback' => 'list_all_urls_rest_fetch_all_urls',
            'args'     => array(
                'type' => array(
                    'validate_callback' => function( $param ) {
                        return is_string( $param );
                    },
                ),
            ),
        )
    );
}

function list_all_urls_rest_fetch_all_urls( $arguments ) {
    if ( isset( $arguments['type'] ) ) {
        $post_type = sanitize_text_field( wp_unslash( $arguments['type'] ) );
    } else {
        $post_type = 'any';
    }

    $args = array(
        'post_type' => $post_type,
    );

    return list_all_urls_generate_url_list( $args );
}

Inside the Block Editor, a simple block can fetch the data using apiFetch:

import { useEffect, useState } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';

export default function Edit() {
    const [ urls, setUrls ] = useState( [] );

    useEffect( () => {
        apiFetch( { path: '/list-all-urls/v1/urls' } ).then( ( response ) => {
            setUrls( response );
        } );
    }, [] );

    if ( ! urls ) {
        return (
            <div { ...useBlockProps() }>
                <p>{ __( 'Loading...', 'list-all-urls' ) }</p>
            </div>
        );
    }

    const urlsList = urls.map( ( url ) => {
        return <li><a href={ url }>{ url }</a></li>;
    } );

    return (
        <div { ...useBlockProps() }>
            <ul>{ urlsList }</ul>
        </div>
    );
}

To display the block correctly on the frontend, you can use a dynamic block with a render.php file:

<?php
/**
 * Render file for the List All URLs block.
 */
$block_attributes = get_block_wrapper_attributes();
$urls             = list_all_urls_generate_url_list( array( 'post_type' => 'any' ), true );

$url_list = '';

foreach ( $urls as $url ) {
    $url_list .= '<li>' . wp_kses_post( $url ) . '</li>';
}
?>

<div <?php echo $block_attributes; ?>>
    <ul>
        <?php echo $url_list; ?>
    </ul>
</div>

This method works but requires many repeated steps: REST routing, block creation, input validation, permission handling, etc. This is where the Abilities API can replace most of the glue code.

Installing the Abilities API for testing

Before WordPress 6.9 is officially released, the Abilities API exists as a plugin and a Composer package, allowing you to test it early. There are three common installation methods:

  • Clone the repository directly into wp-content/plugins and build it.
  • Download the latest release as a zip and install it like a regular plugin.
  • Add the Composer package wordpress/abilities-api to your plugin or theme.

Example of cloning the plugin source:

cd wp-content/plugins
git clone [email protected]:WordPress/abilities-api.git
cd abilities-api
composer install
npm install
npm run build

Or using Composer inside the List All URLs plugin:

cd wp-content/plugins/list-all-urls
composer require wordpress/abilities-api

Once WordPress 6.9 is installed, the server-side parts of the Abilities API are available in core, but using the Composer package remains useful if you want the latest stable version or are working in an environment not yet upgraded.

Registering an Ability in PHP with wp_register_ability

Once the Abilities API is loaded, the first step is to register a new ability. Registration must occur inside the wp_abilities_api_init hook to ensure the registry is ready.

<?php
add_action( 'wp_abilities_api_init', 'list_all_urls_register_abilities' );

/**
 * Register the ability to list all URLs.
 *
 * @return void
 */
function list_all_urls_register_abilities(): void {
    wp_register_ability(
        'list-all-urls/urls',
        array(
            'label'       => __( 'Get All URLs', 'list-all-urls' ),
            'description' => __( 'Retrieves a list of URLs from the WordPress site, optionally as clickable anchor links.', 'list-all-urls' ),
            'category'    => 'site',
            'input_schema'  => array(
                'type'       => 'object',
                'properties' => array(
                    'post_type' => array(
                        'type'        => 'string',
                        'description' => 'The post type to retrieve URLs from (e.g., post, page, custom post type).',
                    ),
                    'posts_per_page' => array(
                        'type'        => 'integer',
                        'description' => 'Number of posts to retrieve. Use -1 to retrieve all posts.',
                    ),
                    'post_status' => array(
                        'type'        => 'string',
                        'description' => 'The status of the posts to retrieve (e.g., publish, draft).',
                    ),
                    'makelinks' => array(
                        'type'        => 'boolean',
                        'description' => 'Whether to return URLs as clickable anchor links.',
                    ),
                ),
            ),
            'output_schema' => array(
                'type'       => 'object',
                'properties' => array(
                    'url' => array(
                        'type'        => 'string',
                        'description' => 'URL or clickable link to the URL.',
                    ),
                ),
            ),
            'execute_callback'    => 'list_all_urls_generate_url_list',
            'permission_callback' => '__return_true',
            'meta' => array(
                'show_in_rest' => true,
            ),
        )
    );
}

In this example, the ability has the identifier list-all-urls/urls, clearly describes its purpose, defines the expected inputs and outputs, and reuses list_all_urls_generate_url_list as the execute_callback, meaning the business logic does not need to change.

Anatomy of wp_register_ability parameters

When registering an ability, pay attention to:

  • label: Human-friendly display name used for discovery tools.
  • description: A short explanation of what the ability does.
  • category: The category for grouping the ability. You may use built-in categories (such as site) or register your own.
  • input_schema: A JSON Schema describing valid input. In this example, input is an object with post_type, posts_per_page, post_status, and makelinks.
  • output_schema: Defines the output format, enabling validation and documentation.
  • execute_callback: The function that will run when the ability executes. In this case, it is the existing core logic.
  • permission_callback: Determines who can execute the ability. Returning false will block execution.
  • meta.show_in_rest: When set to true, the ability is automatically exposed through the wp-abilities REST API namespace.

Using an Ability in PHP: wp_get_ability and execute

Once registered, an ability can be retrieved and executed via the Abilities API.

Example from the List All URLs admin page, replacing direct function calls:

<?php
$input = array(
    'post_type'      => $post_type,
    'posts_per_page' => -1,
    'post_status'    => 'publish',
    'makelinks'      => $makelinks,
);

$urls_ability = wp_get_ability( 'list-all-urls/urls' );

if ( $urls_ability ) {
    $urls = $urls_ability->execute( $input );
}

Benefits include:

  • Execution logic is standardized and schema-driven.
  • REST, JavaScript, and AI agents can reuse the same ability without duplicating logic.
  • You can swap or improve the execute_callback globally without touching every call site.

Discovering and inspecting Abilities using WP-CLI

The Abilities API integrates well with WP-CLI. You can inspect registered abilities during development.

List all abilities:

$ wp shell
wp> $abilities = wp_get_abilities();
wp> var_dump( array_keys( $abilities ) );

Check if a specific ability exists:

$ wp shell
wp> $found = wp_has_ability( 'list-all-urls/urls' );
wp> var_dump( $found ); // bool(true) if registered

Retrieve the full ability object:

$ wp shell
wp> $ability = wp_get_ability( 'list-all-urls/urls' );
wp> var_dump( $ability );

The returned object includes name, description, category, input_schema, output_schema, execute_callback, permission_callback, and meta, which is helpful for debugging or documentation.

Automatic REST API integration with Abilities

When meta.show_in_rest is set to true, WordPress automatically provides REST API endpoints under wp-abilities. Default actions include:

  • List all abilities: GET /wp-json/wp-abilities/v1/abilities
  • Retrieve a single ability: GET /wp-json/wp-abilities/v1/{namespace/ability}
  • Execute an ability: GET or POST /wp-json/wp-abilities/v1/{namespace/ability}/run with input

For the List All URLs plugin, this means that simply enabling show_in_rest exposes the list-all-urls/urls ability through REST without manually defining register_rest_route.

Using an Ability in JavaScript via the Abilities API client

The Abilities API also provides a JavaScript client for executing abilities directly in the browser or inside blocks. Instead of calling a custom REST route with apiFetch, you can run executeAbility from @wordpress/abilities.

Example inside a block’s Edit component:

import { useEffect, useState } from '@wordpress/element';
import { useBlockProps } from '@wordpress/block-editor';
import { executeAbility } from '@wordpress/abilities';
import { __ } from '@wordpress/i18n';

export default function Edit( { attributes } ) {
    const [ urls, setUrls ] = useState( null );

    useEffect( () => {
        executeAbility( 'list-all-urls/urls', {
            post_type: 'any',
            makelinks: attributes.makeLinks,
        } ).then( ( result ) => {
            setUrls( result );
        } );
    }, [ attributes.makeLinks ] );

    if ( ! urls ) {
        return (
            <div { ...useBlockProps() }>
                <p>{ __( 'Loading URLs...', 'list-all-urls' ) }</p>
            </div>
        );
    }

    const urlsList = urls.map( ( url ) => {
        return <li><a href={ url }>{ url }</a></li>;
    } );

    return (
        <div { ...useBlockProps() }>
            <ul>{ urlsList }</ul>
        </div>
    );
}

Advantages:

  • The block does not need to know any REST endpoints, only the ability name and expected data format.
  • The same ability can be reused across multiple blocks or UI components.
  • The validated input/output schemas make automation and AI agents easier to implement.

Applying the Abilities API to existing plugin and theme architecture

If your plugin or theme is built around the REST API, you do not need to rewrite everything. A practical approach is:

  1. Extract core business logic into dedicated service functions, such as get_trending_posts, get_recommendations_for_user, calculate_optimal_release_time.
  2. Register abilities for the key services, using the service functions as execute_callback.
  3. Keep existing REST routes, but call the ability or at least the shared service function.
  4. Gradually migrate blocks and new UI to executeAbility or the Abilities REST endpoints.

This ensures backward compatibility while progressively normalizing your architecture for Abilities API and preparing for future AI integrations.

Benefits and practical impacts of the Abilities API

The Abilities API delivers clear benefits:

  • Standardizes how features are described and exposed across core, plugins, and themes.
  • Reduces duplicated code when building REST routes or JavaScript clients.
  • Improves discoverability through the registry and WP-CLI.
  • Provides automatic REST endpoints with consistent permissions and validation.
  • Lays the foundation for AI agent integration and workflow automation using ability schemas.

The trade-off is that developers must learn a new model and define schemas for inputs, outputs, categories, and permissions. However, for medium-to-large plugins, the long-term benefits outweigh the initial learning curve.

Conclusion

WordPress 6.9 and the Abilities API introduce more than just another feature. They establish a foundation that transforms WordPress from a collection of disconnected functions into a structured, discoverable, and automation-friendly system. Through the List All URLs example, we can see how a familiar feature becomes a standardized ability callable from PHP, REST API, JavaScript, and even AI agents.

Learning and adopting the Abilities API early will help your plugin or theme stay aligned with the future direction of WordPress and the broader ecosystem around it.

Comments


  • No comments yet.

Init Toolbox

Press Ctrl + \ on desktop, or swipe left anywhere on mobile.

Login