Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:25:36 +08:00
commit cfadf66888
24 changed files with 4160 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
# 1. Introduction & Overview
## What is the Abilities API?
The WordPress Abilities API provides a standardized way to register and discover distinct units of functionality within a WordPress site. These units, called "Abilities", represent specific actions or capabilities that components can perform, with clearly defined inputs, outputs, and permissions.
It acts as a central registry, making it easier for different parts of WordPress, third-party plugins, themes, and external systems (like AI agents) to understand and interact with the capabilities available on a specific site.
## Core Concepts
- **Ability:** A distinct piece of functionality with a unique name following the `namespace/ability-name` pattern. Each ability has a human-readable name and description, input/output definitions (using JSON Schema), a category assignment, optional permissions, and an associated callback function for execution. Each registered Ability is an instance of the `WP_Ability` class.
- **Category:** A way to organize related abilities. Each ability must belong to exactly one category. Categories have a slug, label, and description. Each registered category is an instance of the `WP_Ability_Category` class.
- **Registry:** A central, singleton object (`WP_Abilities_Registry`) that holds all registered abilities. It provides methods for registering, unregistering, finding, and querying abilities. Similarly, `WP_Abilities_Category_Registry` manages all registered categories.
- **Callback:** The PHP function or method executed when an ability is called via `WP_Ability::execute()`.
- **Schema:** JSON Schema definitions for an ability's expected input (`input_schema`) and its returned output (`output_schema`). This allows for validation and helps agents understand how to use the ability.
- **Permission Callback:** An optional function that determines if the current user can execute a specific ability.
- **Namespace:** The first part of an ability name (before the slash), typically matching the plugin or component name that registers the ability.
## Goals and Benefits
- **Standardization:** Provides a single, consistent way to expose site capabilities.
- **Discoverability:** Makes functionality easily discoverable by AI systems and automation tools.
- **Validation:** Built-in input/output validation using JSON Schema ensures data integrity.
- **Security:** Permission callbacks provide fine-grained access control.
- **Extensibility:** Simple registration pattern allows any plugin or theme to expose their capabilities.
- **AI-Friendly:** Machine-readable format enables intelligent automation and AI agent interactions.
## Use Cases
- **AI Integration:** Allow AI agents to discover and interact with site capabilities.
- **Plugin Interoperability:** Enable plugins to discover and use each other's functionality.
- **Automation Tools:** Provide programmatic access to site features.
- **API Documentation:** Self-documenting capabilities with schema validation.
- **Developer Tools:** Standardized way to expose plugin functionality.
## Registration Example
```php
// First, register a category, or use one of the existing categories.
add_action( 'abilities_api_categories_init', 'my_plugin_register_category');
function my_plugin_register_category(){
wp_register_ability_category( 'site-information', array(
'label' => __( 'Site Information', 'my-plugin' ),
'description' => __( 'Abilities that provide information about the WordPress site.', 'my-plugin' ),
));
}
// Then, register an ability in that category
add_action( 'abilities_api_init', 'my_plugin_register_ability');
function my_plugin_register_ability(){
wp_register_ability( 'my-plugin/site-info', array(
'label' => __( 'Site Info', 'my-plugin' ),
'description' => __( 'Returns information about this WordPress site', 'my-plugin' ),
'category' => 'site-information',
'input_schema' => array(),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'site_name' => array(
'type' => 'string',
'description' => __( 'The name of the WordPress site', 'my-plugin' )
),
'site_url' => array(
'type' => 'string',
'description' => __( 'The URL of the WordPress site', 'my-plugin' )
),
'active_theme' => array(
'type' => 'string',
'description' => __( 'The active theme of the WordPress site', 'my-plugin' )
),
'active_plugins' => array(
'type' => 'array',
'items' => array(
'type' => 'string'
),
'description' => __( 'List of active plugins on the WordPress site', 'my-plugin' )
),
'php_version' => array(
'type' => 'string',
'description' => __( 'The PHP version of the WordPress site', 'my-plugin' )
),
'wordpress_version' => array(
'type' => 'string',
'description' => __( 'The WordPress version of the site', 'my-plugin' )
)
),
),
'execute_callback' => 'my_plugin_get_siteinfo',
'permission_callback' => function( $input ) {
return current_user_can( 'manage_options' );
},
'meta' => array(
'show_in_rest' => true,
),
) );
}
```
This creates a machine-readable capability that AI systems and automation tools can discover, understand, and execute safely within the bounds of WordPress permissions and validation rules.

View File

@@ -0,0 +1,150 @@
# 2. Getting Started
## Installation
Until the Abilities API is merged into WordPress core, it must be installed before it can be used.
### As a plugin
The easiest way to try and use the Abilities API is to install it as a plugin by downloading the latest release from the [GitHub Releases page](https://github.com/WordPress/abilities-api/releases/latest).
#### With WP-CLI
```bash
wp plugin install https://github.com/WordPress/abilities-api/releases/latest/download/abilities-api.zip
```
#### With WP-Env
```jsonc
// .wp-env.json
{
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
// ... other config ...
"plugins": [
"WordPress/abilities-api"
// ... other plugins ...
]
// ... more config ...
}
```
### As a dependency
Plugin authors and developers may wish to rely on the Abilities API as a dependency in their own projects, before it is merged into core. You can do that in one of the following ways.
#### As a Plugin Dependency (Recommended)
The best way to ensure the Abilities API is available for your plugins is to include it as one of your `Requires Plugins` in your [Plugin header](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/). For example:
```diff
# my-plugin.php
/*
*
* Plugin Name: My Plugin
* Plugin URI: https://example.com/plugins/the-basics/
* Description: Handle the basics with this plugin.
* {all the other plugin header fields...}
+ * Requires Plugins: abilities-api
*/
```
While this is enough to ensure the Abilities API is loaded before your plugin, if you need to ensure specific version requirements or provide users guidance on installing the plugin, you can use the methods described [later on](#checking-availability-with-code)
#### As a Composer dependency
```bash
composer require wordpress/abilities-api
```
### Checking availability with code
To ensure the Abilities API is loaded in your plugin:
```php
if ( ! class_exists( 'WP_Ability' ) ) {
// E.g. add an admin notice about the missing dependency.
add_action( 'admin_notices', static function() {
wp_admin_notice(
// If it's a Composer dependency, you might want to suggest running `composer install` instead.
esc_html__( 'This plugin requires the Abilities API to use. Please install and activate it.', 'my-plugin' ),
'error'
);
} );
return;
}
```
You can also check for specific versions of the Abilities API using the `WP_ABILITIES_API_VERSION` constant:
```php
if ( ! defined( 'WP_ABILITIES_API_VERSION' ) || version_compare( WP_ABILITIES_API_VERSION, '0.1.0', '<' ) ) {
// E.g. add an admin notice about the required version.
add_action( 'admin_notices', static function() {
wp_admin_notice(
esc_html__( 'This plugin requires Abilities API version 0.1.0 or higher. Please update the plugin dependency.', 'my-plugin' ),
'error'
);
} );
return;
}
```
## Basic Usage Example
The below example is for a plugin implementation, but it could also be adapted for a theme's functions.php
```php
<?php
// 1. Define a callback function for your ability.
function my_plugin_get_site_title( array $input = array() ): string {
return get_bloginfo( 'name' );
}
// 2. Register the ability when the Abilities API is initialized.
// Using `abilities_api_init` ensures the API is fully loaded.
add_action( 'abilities_api_init', 'my_plugin_register_abilities' );
function my_plugin_register_abilities() {
wp_register_ability( 'my-plugin/get-site-title', array(
'label' => __( 'Get Site Title', 'my-plugin' ),
'description' => __( 'Retrieves the title of the current WordPress site.', 'my-plugin' ),
'input_schema' => array(
'type' => 'object',
'properties' => array(),
'additionalProperties' => false,
),
'output_schema' => array(
'type' => 'string',
'description' => 'The site title.',
),
'execute_callback' => 'my_plugin_get_site_title',
'permission_callback' => '__return_true', // Everyone can access this
'meta' => array(
'category' => 'site-info',
'show_in_rest' => true, // Optional: expose via REST API
) );
}
// 3. Later, you can retrieve and execute the ability.
add_action( 'admin_init', 'my_plugin_use_ability' );
function my_plugin_use_ability() {
$ability = wp_get_ability( 'my-plugin/get-site-title' );
if ( ! $ability ) {
// Ability not found.
return;
}
$site_title = $ability->execute();
if ( is_wp_error( $site_title ) ) {
// Handle execution error
error_log( 'Execution error: ' . $site_title->get_error_message() );
return;
}
// `$site_title` now holds the result of `get_bloginfo( 'name' )`.
echo 'Site Title: ' . esc_html( $site_title );
}
```

View File

@@ -0,0 +1,282 @@
# 3. Registering Abilities (`wp_register_ability`)
The primary way to add functionality to the Abilities API is by using the `wp_register_ability()` function, typically hooked into the `abilities_api_init` action.
## Function Signature
```php
wp_register_ability( string $id, array $args ): ?\WP_Ability
```
- `$id` (`string`): A unique identifier for the ability.
- `$args` (`array`): An array of arguments defining the ability configuration.
- **Return:** (`?\WP_Ability`) An instance of the registered ability if it was successfully registered, `null` on failure (e.g., invalid arguments, duplicate ID).
## Parameters Explained
The `$args` array accepts the following keys:
- `label` (`string`, **Required**): A human-readable name for the ability. Used for display purposes. Should be translatable.
- `description` (`string`, **Required**): A detailed description of what the ability does, its purpose, and its parameters or return values. This is crucial for AI agents to understand how and when to use the ability. Should be translatable.
- `category` (`string`, **Required**): The slug of the category this ability belongs to. The category must be registered before registering the ability using `wp_register_ability_category()`. Categories help organize and filter abilities by their purpose. See [Registering Categories](7.registering-categories.md) for details.
- `input_schema` (`array`, **Required**): A JSON Schema definition describing the expected input parameters for the ability's execute callback. Used for validation and documentation.
- `output_schema` (`array`, **Required**): A JSON Schema definition describing the expected format of the data returned by the ability. Used for validation and documentation.
- `execute_callback` (`callable`, **Required**): The PHP function or method to execute when this ability is called.
- The callback receives one optional argument: it can have any type as defined in the input schema (e.g., `array`, `object`, `string`, etc.).
- The callback should return the result of the ability's operation or return a `WP_Error` object on failure.
- `permission_callback` (`callable`, **Required**): A callback function to check if the current user has permission to execute this ability.
- The callback receives one optional argument: it can have any type as defined in the input schema (e.g., `array`, `object`, `string`, etc.).
- The callback should return a boolean (`true` if the user has permission, `false` otherwise), or a `WP_Error` object on failure.
- If the input does not validate against the input schema, the permission callback will not be called, and a `WP_Error` will be returned instead.
- `meta` (`array`, **Optional**): An associative array for storing arbitrary additional metadata about the ability.
- `annotations` (`array`, **Optional**): An associative array of annotations providing hints about the ability's behavior characteristics. Supports the following keys:
- `instructions` (`string`, **Optional**): Custom instructions or guidance for using the ability (default: `''`).
- `readonly` (`boolean`, **Optional**): Whether the ability only reads data without modifying its environment (default: `false`).
- `destructive` (`boolean`, **Optional**): Whether the ability may perform destructive updates to its environment. If `true`, the ability may perform any type of modification, including deletions or other destructive changes. If `false`, the ability performs only additive updates (default: `true`).
- `idempotent` (`boolean`, **Optional**): Whether calling the ability repeatedly with the same arguments will have no additional effect on its environment (default: `false`).
- `show_in_rest` (`boolean`, **Optional**): Whether to expose this ability via the REST API. Default: `false`.
- When `true`, the ability will be listed in REST API responses and can be executed via REST endpoints.
- When `false`, the ability will be hidden from REST API listings and cannot be executed via REST endpoints, but remains available for internal PHP usage.
## Ability ID Convention
The `$id` parameter must follow the pattern `namespace/ability-name`:
- **Format:** Must contain only lowercase alphanumeric characters (`a-z`, `0-9`), hyphens (`-`), and one forward slash (`/`) for namespacing.
- **Convention:** Use your plugin slug as the namespace, like `my-plugin/ability-name`.
- **Examples:** `my-plugin/update-settings`, `woocommerce/get-product`, `contact-form/send-message`, `analytics/track-event`
## Code Examples
### Registering a Simple Data Retrieval Ability
```php
add_action( 'abilities_api_init', 'my_plugin_register_site_info_ability' );
function my_plugin_register_site_info_ability() {
wp_register_ability( 'my-plugin/get-site-info', array(
'label' => __( 'Get Site Information', 'my-plugin' ),
'description' => __( 'Retrieves basic information about the WordPress site including name, description, and URL.', 'my-plugin' ),
'category' => 'data-retrieval',
'input_schema' => array(
'type' => 'object',
'properties' => array(),
'additionalProperties' => false
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'name' => array(
'type' => 'string',
'description' => 'Site name'
),
'description' => array(
'type' => 'string',
'description' => 'Site tagline'
),
'url' => array(
'type' => 'string',
'format' => 'uri',
'description' => 'Site URL'
)
)
),
'execute_callback' => function( $input ) {
return array(
'name' => get_bloginfo( 'name' ),
'description' => get_bloginfo( 'description' ),
'url' => home_url()
);
},
'permission_callback' => '__return_true',
'meta' => array(
'annotations' => array(
'readonly' => true,
'destructive' => false
),
),
));
}
```
### Registering an Ability with Input Parameters
```php
add_action( 'abilities_api_init', 'my_plugin_register_update_option_ability' );
function my_plugin_register_update_option_ability() {
wp_register_ability( 'my-plugin/update-option', array(
'label' => __( 'Update WordPress Option', 'my-plugin' ),
'description' => __( 'Updates the value of a WordPress option in the database. Requires manage_options capability.', 'my-plugin' ),
'category' => 'data-modification',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'option_name' => array(
'type' => 'string',
'description' => 'The name of the option to update',
'minLength' => 1
),
'option_value' => array(
'description' => 'The new value for the option'
)
),
'required' => array( 'option_name', 'option_value' ),
'additionalProperties' => false
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'success' => array(
'type' => 'boolean',
'description' => 'Whether the option was successfully updated'
),
'previous_value' => array(
'description' => 'The previous value of the option'
)
)
),
'execute_callback' => function( $input ) {
$option_name = $input['option_name'];
$new_value = $input['option_value'];
$previous_value = get_option( $option_name );
$success = update_option( $option_name, $new_value );
return array(
'success' => $success,
'previous_value' => $previous_value
);
},
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
'meta' => array(
'annotations' => array(
'destructive' => false,
'idempotent' => true
),
),
));
}
```
### Registering an Ability with Plugin Dependencies
```php
add_action( 'abilities_api_init', 'my_plugin_register_woo_stats_ability' );
function my_plugin_register_woo_stats_ability() {
// Only register if WooCommerce is active
if ( ! class_exists( 'WooCommerce' ) ) {
return;
}
wp_register_ability( 'my-plugin/get-woo-stats', array(
'label' => __( 'Get WooCommerce Statistics', 'my-plugin' ),
'description' => __( 'Retrieves basic WooCommerce store statistics including total orders and revenue.', 'my-plugin' ),
'category' => 'ecommerce',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'period' => array(
'type' => 'string',
'enum' => array( 'today', 'week', 'month', 'year' ),
'default' => 'month',
'description' => 'Time period for statistics'
)
),
'additionalProperties' => false
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'total_orders' => array(
'type' => 'integer',
'description' => 'Number of orders in period'
),
'total_revenue' => array(
'type' => 'number',
'description' => 'Total revenue in period'
)
)
),
'execute_callback' => function( $input ) {
$period = $input['period'] ?? 'month';
// Implementation would calculate stats based on period
return array(
'total_orders' => 42,
'total_revenue' => 1250.50
);
},
'permission_callback' => function() {
return current_user_can( 'manage_woocommerce' );
},
'meta' => array(
'requires_plugin' => 'woocommerce',
'category' => 'ecommerce'
)
));
}
```
### Registering an Ability That May Fail
```php
add_action( 'abilities_api_init', 'my_plugin_register_send_email_ability' );
function my_plugin_register_send_email_ability() {
wp_register_ability( 'my-plugin/send-email', array(
'label' => __( 'Send Email', 'my-plugin' ),
'description' => __( 'Sends an email to the specified recipient using WordPress mail functions.', 'my-plugin' ),
'category' => 'communication',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'to' => array(
'type' => 'string',
'format' => 'email',
'description' => 'Recipient email address'
),
'subject' => array(
'type' => 'string',
'minLength' => 1,
'description' => 'Email subject'
),
'message' => array(
'type' => 'string',
'minLength' => 1,
'description' => 'Email message body'
)
),
'required' => array( 'to', 'subject', 'message' ),
'additionalProperties' => false
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'sent' => array(
'type' => 'boolean',
'description' => 'Whether the email was successfully sent'
)
)
),
'execute_callback' => function( $input ) {
$sent = wp_mail(
$input['to'],
$input['subject'],
$input['message']
);
if ( ! $sent ) {
return new \WP_Error(
'email_send_failed',
sprintf( __( 'Failed to send email' ), 'my-plugin' )
);
}
return array( 'sent' => true );
},
'permission_callback' => function() {
return current_user_can( 'publish_posts' );
}
));
}
```

View File

@@ -0,0 +1,211 @@
# 4. Using Abilities (`wp_get_ability`, `wp_get_abilities`)
Once abilities are registered, they can be retrieved and executed using global functions from the Abilities API.
## Getting a Specific Ability (`wp_get_ability`)
To get a single ability object by its name (namespace/ability-name):
```php
/**
* Retrieves a registered ability using Abilities API.
*
* @param string $name The name of the registered ability, with its namespace.
* @return ?WP_Ability The registered ability instance, or null if it is not registered.
*/
function wp_get_ability( string $name ): ?WP_Ability
// Example:
$site_info_ability = wp_get_ability( 'my-plugin/get-site-info' );
if ( $site_info_ability ) {
// Ability exists and is registered
$site_info = $site_info_ability->execute();
if ( is_wp_error( $site_info ) ) {
// Handle WP_Error
echo 'Error: ' . $site_info->get_error_message();
} else {
// Use $site_info array
echo 'Site Name: ' . $site_info['name'];
}
} else {
// Ability not found or not registered
}
```
## Getting All Registered Abilities (`wp_get_abilities`)
To get an array of all registered abilities:
```php
/**
* Retrieves all registered abilities using Abilities API.
*
* @return WP_Ability[] The array of registered abilities.
*/
function wp_get_abilities(): array
// Example: Get all registered abilities
$all_abilities = wp_get_abilities();
foreach ( $all_abilities as $name => $ability ) {
echo 'Ability Name: ' . esc_html( $ability->get_name() ) . "\n";
echo 'Label: ' . esc_html( $ability->get_label() ) . "\n";
echo 'Description: ' . esc_html( $ability->get_description() ) . "\n";
echo "---\n";
}
```
## Executing an Ability (`$ability->execute()`)
Once you have a `WP_Ability` object (usually from `wp_get_ability`), you execute it using the `execute()` method.
```php
/**
* Executes the ability after input validation and running a permission check.
*
* @param mixed $input Optional. The input data for the ability. Defaults to `null`.
* @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
*/
// public function execute( $input = null )
// Example 1: Ability with no input parameters
$ability = wp_get_ability( 'my-plugin/get-site-info' );
if ( $ability ) {
$site_info = $ability->execute(); // No input required
if ( is_wp_error( $site_info ) ) {
// Handle WP_Error
echo 'Error: ' . $site_info->get_error_message();
} else {
// Use $site_info array
echo 'Site Name: ' . $site_info['name'];
}
}
// Example 2: Ability with input parameters
$ability = wp_get_ability( 'my-plugin/update-option' );
if ( $ability ) {
$input = array(
'option_name' => 'blogname',
'option_value' => 'My Updated Site Name',
);
$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
// Handle WP_Error
echo 'Error: ' . $result->get_error_message();
} else {
// Use $result
if ( $result['success'] ) {
echo 'Option updated successfully!';
echo 'Previous value: ' . $result['previous_value'];
}
}
}
// Example 3: Ability with complex input validation
$ability = wp_get_ability( 'my-plugin/send-email' );
if ( $ability ) {
$input = array(
'to' => 'user@example.com',
'subject' => 'Hello from WordPress',
'message' => 'This is a test message from the Abilities API.',
);
$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
// Handle WP_Error
echo 'Error: ' . $result->get_error_message();
} elseif ( $result['sent'] ) {
echo 'Email sent successfully!';
} else {
echo 'Email failed to send.';
}
}
```
## Checking Permissions (`$ability->check_permissions()`)
You can check if the current user has permissions to execute the ability, also without executing it. The `check_permissions()` method returns either `true`, `false`, or a `WP_Error` object. `true` means permission is granted, `false` means the user simply lacks permission, and a `WP_Error` return value typically indicates a failure in the permission check process (such as an internal error or misconfiguration). You must use `is_wp_error()` to handle errors properly and distinguish between permission denial and actual errors:
```php
$ability = wp_get_ability( 'my-plugin/update-option' );
if ( $ability ) {
$input = array(
'option_name' => 'blogname',
'option_value' => 'New Site Name',
);
// Check permission before execution - always use is_wp_error() first
$has_permissions = $ability->check_permissions( $input );
if ( true === $has_permissions ) {
// Permissions granted safe to execute.
echo 'You have permissions to execute this ability.';
} else {
// Don't leak permission errors to unauthenticated users.
if ( is_wp_error( $has_permissions ) ) {
error_log( 'Permissions check failed: ' . $has_permissions->get_error_message() );
}
echo 'You do not have permissions to execute this ability.';
}
}
```
## Inspecting Ability Properties
The `WP_Ability` class provides several getter methods to inspect ability properties:
```php
$ability = wp_get_ability( 'my-plugin/get-site-info' );
if ( $ability ) {
// Basic properties
echo 'Name: ' . $ability->get_name() . "\n";
echo 'Label: ' . $ability->get_label() . "\n";
echo 'Description: ' . $ability->get_description() . "\n";
// Schema information
$input_schema = $ability->get_input_schema();
$output_schema = $ability->get_output_schema();
echo 'Input Schema: ' . json_encode( $input_schema, JSON_PRETTY_PRINT ) . "\n";
echo 'Output Schema: ' . json_encode( $output_schema, JSON_PRETTY_PRINT ) . "\n";
// Metadata
$meta = $ability->get_meta();
if ( ! empty( $meta ) ) {
echo 'Metadata: ' . json_encode( $meta, JSON_PRETTY_PRINT ) . "\n";
}
}
```
## Error Handling Patterns
The Abilities API uses several error handling mechanisms:
```php
$ability = wp_get_ability( 'my-plugin/some-ability' );
if ( ! $ability ) {
// Ability not registered
echo 'Ability not found';
return;
}
$result = $ability->execute( $input );
// Check for WP_Error (validation, permission, or callback errors)
if ( is_wp_error( $result ) ) {
echo 'WP_Error: ' . $result->get_error_message();
return;
}
// Check for null result (permission denied, invalid callback, or validation failure)
if ( is_null( $result ) ) {
echo 'Execution returned null - check permissions and callback validity';
return;
}
// Success - use the result
// Process $result based on the ability's output schema
```

View File

@@ -0,0 +1,345 @@
# 5. REST API Reference
The WordPress Abilities API provides REST endpoints that allow external systems to discover and execute abilities via HTTP requests.
## User access
Access to all Abilities REST API endpoints requires an authenticated user (see the [Authentication](#authentication) section). Access to execute individual Abilities is restricted based on the `permission_callback()` of the Ability.
## Controlling REST API Exposure
By default, registered abilities are **not** exposed via the REST API. You can control whether an individual ability appears in the REST API by using the `show_in_rest` meta when registering the ability:
- `show_in_rest => true`: The ability is listed in REST API responses and can be executed via REST endpoints.
- `show_in_rest => false` (default): The ability is hidden from REST API listings and cannot be executed via REST endpoints. The ability remains available for internal PHP usage via `wp_execute_ability()`.
Abilities with meta `show_in_rest => false` will return a `rest_ability_not_found` error if accessed via REST endpoints.
## Schema
The Abilities API endpoints are available under the `/wp/v2/abilities` namespace.
### Ability Object
Abilities are represented in JSON with the following structure:
```json
{
"name": "my-plugin/get-site-info",
"label": "Get Site Information",
"description": "Retrieves basic information about the WordPress site.",
"category": "site-information",
"output_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Site name"
},
"url": {
"type": "string",
"format": "uri",
"description": "Site URL"
}
}
},
"meta": {
"annotations": {
"instructions": "",
"readonly": true,
"destructive": false,
"idempotent": false
}
}
}
```
## List Abilities
### Definition
`GET /wp/v2/abilities`
### Arguments
- `page` _(integer)_: Current page of the collection. Default: `1`.
- `per_page` _(integer)_: Maximum number of items to return per page. Default: `50`, Maximum: `100`.
- `category` _(string)_: Filter abilities by category slug.
### Example Request
```bash
curl https://example.com/wp-json/wp/v2/abilities
```
### Example Response
```json
[
{
"name": "my-plugin/get-site-info",
"label": "Get Site Information",
"description": "Retrieves basic information about the WordPress site.",
"category": "site-information",
"output_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Site name"
},
"url": {
"type": "string",
"format": "uri",
"description": "Site URL"
}
}
},
"meta": {
"annotations": {
"instructions": "",
"readonly": false,
"destructive": true,
"idempotent": false
}
}
}
]
```
## List Categories
### Definition
`GET /wp/v2/abilities/categories`
### Arguments
- `page` _(integer)_: Current page of the collection. Default: `1`.
- `per_page` _(integer)_: Maximum number of items to return per page. Default: `50`, Maximum: `100`.
### Example Request
```bash
curl -u 'USERNAME:APPLICATION_PASSWORD' \
https://example.com/wp-json/wp/v2/abilities/categories
```
### Example Response
```json
[
{
"slug": "data-retrieval",
"label": "Data Retrieval",
"description": "Abilities that retrieve and return data from the WordPress site.",
"meta": {},
"_links": {
"self": [
{
"href": "https://example.com/wp-json/wp/v2/abilities/categories/data-retrieval"
}
],
"collection": [
{
"href": "https://example.com/wp-json/wp/v2/abilities/categories"
}
],
"abilities": [
{
"href": "https://example.com/wp-json/wp/v2/abilities?category=data-retrieval"
}
]
}
}
]
```
## Retrieve a Category
### Definition
`GET /wp/v2/abilities/categories/{slug}`
### Arguments
- `slug` _(string)_: The unique slug of the category.
### Example Request
```bash
curl -u 'USERNAME:APPLICATION_PASSWORD' \
https://example.com/wp-json/wp/v2/abilities/categories/data-retrieval
```
### Example Response
```json
{
"slug": "data-retrieval",
"label": "Data Retrieval",
"description": "Abilities that retrieve and return data from the WordPress site.",
"meta": {},
"_links": {
"self": [
{
"href": "https://example.com/wp-json/wp/v2/abilities/categories/data-retrieval"
}
],
"collection": [
{
"href": "https://example.com/wp-json/wp/v2/abilities/categories"
}
],
"abilities": [
{
"href": "https://example.com/wp-json/wp/v2/abilities?category=data-retrieval"
}
]
}
}
```
## Retrieve an Ability
### Definition
`GET /wp/v2/abilities/(?P<namespace>[a-z0-9-]+)/(?P<ability>[a-z0-9-]+)`
### Arguments
- `namespace` _(string)_: The namespace part of the ability name.
- `ability` _(string)_: The ability name part.
### Example Request
```bash
curl https://example.com/wp-json/wp/v2/abilities/my-plugin/get-site-info
```
### Example Response
```json
{
"name": "my-plugin/get-site-info",
"label": "Get Site Information",
"description": "Retrieves basic information about the WordPress site.",
"category": "site-information",
"output_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Site name"
},
"url": {
"type": "string",
"format": "uri",
"description": "Site URL"
}
}
},
"meta": {
"annotations": {
"instructions": "",
"readonly": true,
"destructive": false,
"idempotent": false
}
}
}
```
## Execute an Ability
Abilities are executed via the `/run` endpoint. The required HTTP method depends on the ability's `readonly` annotation:
- **Read-only abilities** (`readonly: true`) must use **GET**
- **Regular abilities** (default) must use **POST**
This distinction ensures read-only operations use safe HTTP methods that can be cached and don't modify server state.
### Definition
`GET|POST /wp/v2/abilities/(?P<namespace>[a-z0-9-]+)/(?P<ability>[a-z0-9-]+)/run`
### Arguments
- `namespace` _(string)_: The namespace part of the ability name.
- `ability` _(string)_: The ability name part.
- `input` _(integer|number|boolean|string|array|object|null)_: Optional input data for the ability as defined by its input schema.
- For **GET requests**: pass as `input` query parameter (URL-encoded JSON)
- For **POST requests**: pass in JSON body
### Example Request (Read-only, GET)
```bash
# No input
curl https://example.com/wp-json/wp/v2/abilities/my-plugin/get-site-info/run
# With input (URL-encoded)
curl "https://example.com/wp-json/wp/v2/abilities/my-plugin/get-user-info/run?input=%7B%22user_id%22%3A1%7D"
```
### Example Request (Regular, POST)
```bash
# No input
curl -X POST https://example.com/wp-json/wp/v2/abilities/my-plugin/create-draft/run
# With input
curl -X POST \
-H "Content-Type: application/json" \
-d '{"input":{"option_name":"blogname","option_value":"New Site Name"}}' \
https://example.com/wp-json/wp/v2/abilities/my-plugin/update-option/run
```
### Example Response (Success)
```json
{
"name": "My WordPress Site",
"url": "https://example.com"
}
```
### Example Response (Error)
```json
{
"code": "ability_invalid_permissions",
"message": "Ability \"my-plugin/update-option\" does not have necessary permission.",
"data": {
"status": 403
}
}
```
## Authentication
The Abilities API supports all WordPress REST API authentication methods:
- Cookie authentication (same-origin requests)
- Application passwords (recommended for external access)
- Custom authentication plugins
### Using Application Passwords
```bash
curl -u 'USERNAME:APPLICATION_PASSWORD' \
https://example.com/wp-json/wp/v2/abilities
```
## Error Responses
The API returns standard WordPress REST API error responses with these common codes:
- `ability_missing_input_schema` the ability requires input but none was provided.
- `ability_invalid_input` - input validation failed according to the ability's schema.
- `ability_invalid_permissions` - current user lacks permission to execute the ability.
- `ability_invalid_output` - output validation failed according to the ability's schema.
- `ability_invalid_execute_callback` - the ability's execute callback is not callable.
- `rest_ability_not_found` - the requested ability is not registered.
- `rest_category_not_found` - the requested category is not registered.
- `rest_ability_invalid_method` - the requested HTTP method is not allowed for executing the selected ability (e.g., using GET on a read-only ability, or POST on a regular ability).
- `rest_ability_cannot_execute` - the ability cannot be executed due to insufficient permissions.

View File

@@ -0,0 +1,204 @@
# 6. Hooks
The Abilities API provides [WordPress Action and Filter Hooks](https://developer.wordpress.org/apis/hooks/) that allow developers to monitor and respond to ability execution events.
## Quick Links
- [Actions](#actions)
- [`abilities_api_categories_init`](#abilities_api_categories_init)
- [`before_execute_ability`](#before_execute_ability)
- [`after_execute_ability`](#after_execute_ability)
- [Filters](#filters)
- [`register_ability_args`](#register_ability_args)
- [`register_ability_category_args`](#register_ability_category_args)
## Actions
### `abilities_api_categories_init`
Fires when the category registry is first initialized. This is the proper hook to use when registering categories.
```php
do_action( 'abilities_api_categories_init', $registry );
```
#### Parameters
- `$registry` (`\WP_Abilities_Category_Registry`): The category registry instance.
#### Usage Example
```php
/**
* Register custom ability categories.
*
* @param \WP_Abilities_Category_Registry $registry The category registry instance.
*/
function my_plugin_register_categories( $registry ) {
wp_register_ability_category( 'ecommerce', array(
'label' => __( 'E-commerce', 'my-plugin' ),
'description' => __( 'Abilities related to e-commerce functionality.', 'my-plugin' ),
));
wp_register_ability_category( 'analytics', array(
'label' => __( 'Analytics', 'my-plugin' ),
'description' => __( 'Abilities that provide analytical data and insights.', 'my-plugin' ),
));
}
add_action( 'abilities_api_categories_init', 'my_plugin_register_categories' );
```
### `before_execute_ability`
Fires immediately before an ability gets executed, after permission checks have passed but before the execution callback is called.
```php
do_action( 'before_execute_ability', $ability_name, $input );
```
#### Parameters
- `$ability_name` (`string`): The namespaced name of the ability being executed (e.g., `my-plugin/get-posts`).
- `$input` (`mixed`): The input data passed to the ability.
#### Usage Example
```php
/**
* Log each ability execution attempt.
* @param string $ability_name The name of the ability being executed.
* @param mixed $input The input data passed to the ability.
*/
function log_ability_execution( string $ability_name, $input ) {
error_log( 'About to execute ability: ' . $ability_name );
if ( $input !== null ) {
error_log( 'Input: ' . wp_json_encode( $input ) );
}
}
add_action( 'before_execute_ability', 'log_ability_execution', 10, 2 );
```
### `after_execute_ability`
Fires immediately after an ability has finished executing successfully, after output validation has passed.
```php
do_action( 'after_execute_ability', string $ability_name, $input, $result );
```
#### Parameters
- `$ability_name` (`string`): The namespaced name of the ability that was executed.
- `$input` (`mixed`): The input data that was passed to the ability.
- `$result` (`mixed`): The validated result returned by the ability's execution callback.
#### Usage Example
```php
/**
* Log the result of each ability execution.
*
* @param string $ability_name The name of the executed ability.
* @param mixed $input The input data passed to the ability.
* @param mixed $result The result returned by the ability.
*/
function log_ability_result( string $ability_name, $input, $result ) {
error_log( 'Completed ability: ' . $ability_name );
error_log( 'Result: ' . wp_json_encode( $result ) );
}
add_action( 'after_execute_ability', 'log_ability_result', 10, 3 );
```
## Filters
### `register_ability_args`
Allows modification of an Ability's args before they are validated and used to instantiate the Ability.
```php
$args = apply_filters( 'register_ability_args', array $args, string $ability_name );
```
#### Parameters
- `$args` (`array<string,mixed>`): The arguments used to instantiate the ability. See [wp_register_ability()](./3.registering-abilities.md#wp_register_ability) for the full list of args.
- `$ability_name` (`string`): The namespaced name of the ability being registered (e.g., `my-plugin/get-posts`).
#### Usage Example
```php
/**
* Modify ability args before validation.
*
* @param array<string,mixed> $args The arguments used to instantiate the ability.
* @param string $ability_name The name of the ability, with its namespace.
*
* @return array<string,mixed> The modified ability arguments.
*/
function my_modify_ability_args( array $args, string $ability_name ): array {
// Check if the ability name matches what you're looking for.
if ( 'my-namespace/my-ability' !== $ability_name ) {
return $args;
}
// Modify the args as needed.
$args['label'] = __('My Custom Ability Label');
// You can use the old args to build new ones.
$args['description'] = sprintf(
/* translators: 1: Ability name 2: Previous description */
__('This is a custom description for the ability %s. Previously the description was %s', 'text-domain'),
$ability_name,
$args['description'] ?? 'N/A'
);
// Even if they're callbacks.
$args['permission_callback' ] = static function ( $input = null ) use ( $args, $ability_name ) {
$previous_check = is_callable( $args['permission_callback'] ) ? $args['permission_callback']( $input ) : true;
// If we already failed, no need for stricter checks.
if ( ! $previous_check || is_wp_error( $previous_check ) ) {
return $previous_check;
}
return current_user_can( 'my_custom_ability_cap', $ability_name );
}
return $args;
}
add_filter( 'register_ability_args', 'my_modify_ability_args', 10, 2 );
```
### `register_ability_category_args`
Allows modification of a category's arguments before validation.
```php
$args = apply_filters( 'register_ability_category_args', array $args, string $slug );
```
#### Parameters
- `$args` (`array<string,mixed>`): The arguments used to instantiate the category (label, description).
- `$slug` (`string`): The slug of the category being registered.
#### Usage Example
```php
/**
* Modify category args before validation.
*
* @param array<string,mixed> $args The arguments used to instantiate the category.
* @param string $slug The slug of the category being registered.
*
* @return array<string,mixed> The modified category arguments.
*/
function my_modify_category_args( array $args, string $slug ): array {
if ( 'my-category' === $slug ) {
$args['label'] = __( 'My Custom Label', 'my-plugin' );
$args['description'] = __( 'My custom description for this category.', 'my-plugin' );
}
return $args;
}
add_filter( 'register_ability_category_args', 'my_modify_category_args', 10, 2 );
```

View File

@@ -0,0 +1,298 @@
# 7. JavaScript/TypeScript Client
The JavaScript client provides an interface for discovering and executing WordPress Abilities from the browser.
## Overview
The JavaScript client enables frontend code to interact with the Abilities API system. It can:
- Discover all registered abilities on your WordPress site
- Execute server-side PHP abilities
- Register and execute client-side JavaScript abilities
You can read more about installation and setup in the [package readme](../packages/client/README.md).
## Core API Functions
### `getAbilities( args = {} )`
Returns an array of all registered abilities (both server-side and client-side).
**Parameters:** `args` (object, optional) - Query arguments to filter abilities. Supported arguments:
- `category` (string) - Filter abilities by category slug
**Returns:** `Promise<Array>` - Array of ability objects
**Example:**
```javascript
import { getAbilities } from `@wordpress/abilities`;
const abilities = await getAbilities();
console.log(`Found ${abilities.length} abilities`);
// List all abilities
abilities.forEach(ability => {
console.log(`${ability.name}: ${ability.description}`);
});
// Get abilities in a specific category
const dataAbilities = await getAbilities( { category: 'data-retrieval' } );
console.log( `Found ${ dataAbilities.length } data retrieval abilities` );
```
### getAbility( name )
Retrieves a specific ability by name.
**Parameters:**
- `name` (string) - The ability name (e.g., 'my-plugin/get-posts')
**Returns:** `Promise<Object|null>` - The ability object or null if not found
**Example:**
```javascript
const ability = await getAbility( 'my-plugin/get-site-info' );
if ( ability ) {
console.log( 'Label:', ability.label );
console.log( 'Description:', ability.description );
console.log( 'Input Schema:', ability.input_schema );
}
```
### `getAbilityCategories()`
Returns an array of all registered ability categories.
**Parameters:** None
**Returns:** `Promise<Array>` - Array of category objects
**Example:**
```javascript
const categories = await getAbilityCategories();
console.log( `Found ${ categories.length } categories` );
// List all categories
categories.forEach( ( category ) => {
console.log( `${ category.label }: ${ category.description }` );
} );
```
### `getAbilityCategory( slug )`
Retrieves a specific category by slug.
**Parameters:**
- `slug` (string) - The category slug (e.g., 'data-retrieval')
**Returns:** `Promise<Object|null>` - The category object or null if not found
**Example:**
```javascript
const category = await getAbilityCategory( 'data-retrieval' );
if ( category ) {
console.log( 'Label:', category.label );
console.log( 'Description:', category.description );
}
```
### `executeAbility( name, input = null )`
Executes an ability with the provided input data.
**Parameters:**
- `name` (string) - The ability name
- `input` (any, optional) - Input data for the ability
**Returns:** `Promise<any>` - The ability's output
**Example:**
```javascript
// Execute without input
const siteTitle = await executeAbility( 'my-plugin/get-site-title' );
console.log( 'Site:', siteTitle );
// Execute with input parameters
const posts = await executeAbility( 'my-plugin/get-posts', {
category: 'news',
limit: 5,
} );
posts.forEach( ( post ) => console.log( post.title ) );
```
### `registerAbility( ability )`
Registers a client-side ability that runs in the browser.
**Parameters:**
- `ability` (object) - The ability configuration object
**Returns:** `Promise<void>`
**Example:**
```javascript
// showNotification function
const showNotification = ( message ) => {
new Notification( message );
return { success: true, displayed: message };
};
// Register a notification ability which calls the showNotification function
await registerAbility( {
name: 'my-plugin/show-notification',
label: 'Show Notification',
description: 'Display a notification message to the user',
input_schema: {
type: 'object',
properties: {
message: { type: 'string' },
type: { type: 'string', enum: [ 'success', 'error', 'warning', 'info' ] },
},
required: [ 'message' ],
},
callback: async ( { message, type = 'info' } ) => {
// Show browser notification
if ( ! ( 'Notification' in window ) ) {
alert( 'This browser does not support desktop notification' );
return {
success: false,
error: 'Browser does not support notifications',
};
}
if ( Notification.permission !== 'granted' ) {
Notification.requestPermission().then( ( permission ) => {
if ( permission === 'granted' ) {
return showNotification( message );
}
} );
}
return showNotification( message );
},
permissionCallback: () => {
return !! wp.data.select( 'core' ).getCurrentUser();
},
} );
// Use the registered ability
const result = await executeAbility( 'my-plugin/show-notification', {
message: 'Hello World!',
type: 'success',
} );
```
### `unregisterAbility( name )`
Removes a previously registered client-side ability.
**Parameters:**
- `name` (string) - The ability name to unregister
**Returns:** `void`
**Example:**
```javascript
// Unregister an ability
unregisterAbility( 'my-plugin/old-ability' );
```
### `registerAbilityCategory( slug, args )`
Registers a client-side ability category. This is useful when registering client-side abilities that introduce new categories not defined by the server.
**Parameters:**
- `slug` (string) - The category slug (lowercase alphanumeric with dashes only)
- `args` (object) - Category configuration object
- `label` (string) - Human-readable label for the category
- `description` (string) - Detailed description of the category
- `meta` (object, optional) - Optional metadata about the category
**Returns:** `Promise<void>`
**Example:**
```javascript
// Register a new category
await registerAbilityCategory( 'block-editor', {
label: 'Block Editor',
description: 'Abilities for interacting with the WordPress block editor',
} );
// Register a category with metadata
await registerAbilityCategory( 'custom-category', {
label: 'Custom Category',
description: 'A category for custom abilities',
meta: {
priority: 'high',
icon: 'dashicons-admin-customizer',
},
} );
// Then register abilities using the new category
await registerAbility( {
name: 'my-plugin/insert-block',
label: 'Insert Block',
description: 'Inserts a block into the editor',
category: 'block-editor', // Uses the client-registered category
callback: async ( { blockType } ) => {
// Implementation
return { success: true };
},
} );
```
### `unregisterAbilityCategory( slug )`
Removes a previously registered client-side category.
**Parameters:**
- `slug` (string) - The category slug to unregister
**Returns:** `void`
**Example:**
```javascript
// Unregister a category
unregisterAbilityCategory( 'block-editor' );
```
## Error Handling
All functions return promises that may reject with specific error codes:
```javascript
try {
const result = await executeAbility( 'my-plugin/restricted-action', input );
console.log( 'Success:', result );
} catch ( error ) {
switch ( error.code ) {
case 'ability_permission_denied':
console.error( 'Permission denied:', error.message );
break;
case 'ability_invalid_input':
console.error( 'Invalid input:', error.message );
break;
case 'rest_ability_not_found':
console.error( 'Ability not found:', error.message );
break;
default:
console.error( 'Execution failed:', error.message );
}
}
```

View File

@@ -0,0 +1,61 @@
# 7. Registering Categories
Before registering abilities, you must register at least one category. Categories help organize abilities and make them easier to discover and filter.
## Function Signature
```php
wp_register_ability_category( string $slug, array $args ): ?\WP_Ability_Category
```
**Parameters:**
- `$slug` (`string`): A unique identifier for the category. Must contain only lowercase alphanumeric characters and dashes (no underscores, no uppercase).
- `$args` (`array`): Category configuration with these keys:
- `label` (`string`, **Required**): Human-readable name for the category. Should be translatable.
- `description` (`string`, **Required**): Detailed description of the category's purpose. Should be translatable.
- `meta` (`array`, **Optional**): An associative array for storing arbitrary additional metadata about the category.
**Return:** (`?\WP_Ability_Category`) An instance of the registered category if it was successfully registered, `null` on failure (e.g., invalid arguments, duplicate slug).
**Note:** Categories must be registered during the `abilities_api_categories_init` action hook.
## Code Example
```php
add_action( 'abilities_api_categories_init', 'my_plugin_register_categories' );
function my_plugin_register_categories() {
wp_register_ability_category( 'data-retrieval', array(
'label' => __( 'Data Retrieval', 'my-plugin' ),
'description' => __( 'Abilities that retrieve and return data from the WordPress site.', 'my-plugin' ),
));
wp_register_ability_category( 'data-modification', array(
'label' => __( 'Data Modification', 'my-plugin' ),
'description' => __( 'Abilities that modify data on the WordPress site.', 'my-plugin' ),
));
wp_register_ability_category( 'communication', array(
'label' => __( 'Communication', 'my-plugin' ),
'description' => __( 'Abilities that send messages or notifications.', 'my-plugin' ),
));
}
```
## Category Slug Convention
The `$slug` parameter must follow these rules:
- **Format:** Must contain only lowercase alphanumeric characters (`a-z`, `0-9`) and hyphens (`-`).
- **Valid examples:** `data-retrieval`, `ecommerce`, `site-information`, `user-management`, `category-123`
- **Invalid examples:**
- Uppercase: `Data-Retrieval`, `MyCategory`
- Underscores: `data_retrieval`
- Special characters: `data.retrieval`, `data/retrieval`, `data retrieval`
- Leading/trailing dashes: `-data`, `data-`
- Double dashes: `data--retrieval`
## Other Category Functions
- `wp_unregister_ability_category( string $slug )` - Remove a registered category. Returns the unregistered category instance or `null` on failure.
- `wp_get_ability_category( string $slug )` - Retrieve a specific category by slug. Returns the category instance or `null` if not found.
- `wp_get_ability_categories()` - Get all registered categories as an associative array keyed by slug.