WordPress is introducing two new interconnected systems for AI integration: the Abilities API and the Connectors system. Together, they provide a standardized way to define executable WordPress capabilities and manage API credentials for external AI providers.
This guide covers both systems from a developer perspective, including architecture, API reference, and practical examples.
The two systems serve complementary roles:
-
Abilities API (
@wordpress/abilities) — A registry of discoverable, executable WordPress capabilities. Abilities are structured actions (like "create a post" or "navigate to a URL") with defined input/output schemas, permission checks, and validation. They can be registered on the server (PHP) or client (JavaScript) and are designed to be consumed by AI assistants, plugins, or any automation that needs to discover what WordPress can do. -
Connectors (
@wordpress/connectors) — A centralized system for managing API credentials for external AI service providers. It provides a unified Settings page where users configure API keys once, which are then shared across all plugins. Connectors integrates with WordPress's coreAiClientclass to authenticate requests to providers like OpenAI, Anthropic, and Google.
The Abilities API follows a client-server model with two JavaScript packages:
┌─────────────────────────────────────────────────────┐
│ WordPress Server │
│ (PHP: WP_Ability, WP_Ability_Category) │
│ REST API: /wp-abilities/v1/ │
└──────────────────────┬──────────────────────────────┘
│ REST
┌──────────────────────▼──────────────────────────────┐
│ @wordpress/core-abilities │
│ Fetches server abilities & categories on load │
│ Creates REST-backed callbacks for server abilities │
└──────────────────────┬──────────────────────────────┘
│ registers into
┌──────────────────────▼──────────────────────────────┐
│ @wordpress/abilities │
│ In-memory store (core/abilities) │
│ Client-side registration, validation, execution │
│ @wordpress/data integration │
└─────────────────────────────────────────────────────┘
Key design decisions:
- Unified store — Both server-registered and client-registered abilities coexist in the same
core/abilitiesdata store. - Validation at every layer — Names, categories, input, and output are all validated using JSON Schema (draft-04).
- Metadata annotations — Abilities carry metadata (e.g.
readonly,destructive,idempotent) that controls both behavior and HTTP method selection for server-side execution.
| Package | Store Name | Purpose |
|---|---|---|
@wordpress/abilities |
core/abilities |
Core client library. Registers, queries, validates, and executes abilities. |
@wordpress/core-abilities |
— | Integration layer. Fetches server-side abilities/categories via REST API and registers them into the client store with REST-backed callbacks. |
An ability is a named, categorized action that WordPress (or a plugin) can perform. Each ability has:
| Property | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Unique identifier in namespace format (e.g. my-plugin/create-post). |
label |
string |
Yes | Human-readable label. |
description |
string |
Yes | Detailed description of what the ability does. |
category |
string |
Yes | Slug of a registered category. |
input_schema |
object |
No | JSON Schema for input validation. |
output_schema |
object |
No | JSON Schema for output validation. |
callback |
function |
Yes* | Execution function. (*Required for client-side; auto-generated for server abilities.) |
permissionCallback |
function |
No | Pre-execution permission check. |
meta |
object |
No | Metadata including annotations. |
Categories organize abilities into logical groups. They must be registered before any abilities that reference them.
| Property | Type | Required | Description |
|---|---|---|---|
slug |
string |
Yes | Unique identifier (lowercase alphanumeric with dashes). |
label |
string |
Yes | Human-readable label. |
description |
string |
Yes | Description of the category. |
meta |
object |
No | Metadata including annotations. |
Ability names follow a namespaced format with 2–4 segments:
Pattern: /^[a-z0-9-]+(?:\/[a-z0-9-]+){1,3}$/
Valid:
my-plugin/my-ability (2 segments)
core/posts/find (3 segments)
my-plugin/resource/sub/action (4 segments)
Invalid:
my-ability (1 segment — no namespace)
My-Plugin/My-Ability (uppercase)
my_plugin/my_ability (underscores)
Category slugs use lowercase alphanumeric characters and dashes:
Pattern: /^[a-z0-9]+(?:-[a-z0-9]+)*$/
Valid: data-retrieval, user-management, block-editor
Invalid: Data_Retrieval, --leading-dash, trailing-dash-
Register a category before registering abilities that belong to it:
import { registerAbilityCategory } from '@wordpress/abilities';
registerAbilityCategory( 'block-editor', {
label: 'Block Editor',
description: 'Abilities for interacting with the WordPress block editor',
} );
// Categories can include optional metadata
registerAbilityCategory( 'custom-category', {
label: 'Custom Category',
description: 'A category for custom abilities',
meta: {
color: '#ff0000',
},
} );To remove a category:
import { unregisterAbilityCategory } from '@wordpress/abilities';
unregisterAbilityCategory( 'block-editor' );Register a client-side ability with a callback function:
import { registerAbility } from '@wordpress/abilities';
registerAbility( {
name: 'my-plugin/navigate',
label: 'Navigate to URL',
description: 'Navigates to a URL within the WordPress admin',
category: 'navigation',
input_schema: {
type: 'object',
properties: {
url: { type: 'string' },
},
required: [ 'url' ],
},
callback: async ( { url } ) => {
window.location.href = url;
return { success: true };
},
} );Important: The category (navigation in this example) must already be registered before the ability can be registered. If it doesn't exist, an error is thrown.
To remove an ability:
import { unregisterAbility } from '@wordpress/abilities';
unregisterAbility( 'my-plugin/navigate' );The store validates every registration and throws descriptive errors:
- Name is required and must match the naming pattern.
- Label and description are required.
- Category must reference an already-registered category.
- Callback (if provided) must be a function.
- Duplicate names are rejected.
Use executeAbility to run any registered ability (client or server):
import { executeAbility } from '@wordpress/abilities';
try {
const result = await executeAbility( 'my-plugin/create-item', {
title: 'New Item',
content: 'Item content',
} );
console.log( 'Result:', result );
} catch ( error ) {
console.error( 'Execution failed:', error.message );
// error.code may be:
// 'ability_permission_denied'
// 'ability_invalid_input'
// 'ability_invalid_output'
}Execution flow:
- Look up the ability by name.
- Run the
permissionCallback(if defined). Throw if denied. - Validate input against
input_schema(if defined). Throw if invalid. - Call the ability's
callback. - Validate output against
output_schema(if defined). Throw if invalid. - Return the result.
For server-side abilities, steps 3 and 5 are handled by the server — the client delegates validation to the REST API.
The Abilities API integrates with @wordpress/data for reactive use in React components:
import { useSelect } from '@wordpress/data';
import { store as abilitiesStore } from '@wordpress/abilities';
function AbilitiesList() {
// Get all abilities
const allAbilities = useSelect(
( select ) => select( abilitiesStore ).getAbilities(),
[]
);
// Get abilities filtered by category
const dataAbilities = useSelect(
( select ) =>
select( abilitiesStore ).getAbilities( {
category: 'data-retrieval',
} ),
[]
);
// Get a specific ability
const specificAbility = useSelect(
( select ) =>
select( abilitiesStore ).getAbility( 'my-plugin/my-ability' ),
[]
);
// Get all categories
const categories = useSelect(
( select ) => select( abilitiesStore ).getAbilityCategories(),
[]
);
// Get a specific category
const dataCategory = useSelect(
( select ) =>
select( abilitiesStore ).getAbilityCategory( 'data-retrieval' ),
[]
);
return (
<div>
<h2>All Abilities ({ allAbilities.length })</h2>
<ul>
{ allAbilities.map( ( ability ) => (
<li key={ ability.name }>
<strong>{ ability.label }</strong>: { ability.description }
</li>
) ) }
</ul>
</div>
);
}Available selectors:
| Selector | Arguments | Returns |
|---|---|---|
getAbilities( args? ) |
{ category?: string } |
Ability[] |
getAbility( name ) |
string |
Ability | undefined |
getAbilityCategories() |
— | AbilityCategory[] |
getAbilityCategory( slug ) |
string |
AbilityCategory | undefined |
Abilities can define JSON Schema (draft-04) for both input and output. The built-in validator supports:
- Type validation (
string,number,integer,boolean,array,object,null) - Required fields
- Enums and patterns
- String formats (
email,date-time,uuid,ipv4,ipv6,hostname) - Number constraints (
minimum,maximum,multipleOf) - Array constraints (
minItems,maxItems,uniqueItems) - Object constraints (
minProperties,maxProperties) - Combinators (
anyOf,oneOf)
Example with schemas:
registerAbility( {
name: 'my-plugin/create-post',
label: 'Create Post',
description: 'Creates a new WordPress post',
category: 'content-management',
input_schema: {
type: 'object',
properties: {
title: { type: 'string', minLength: 1 },
content: { type: 'string' },
status: {
type: 'string',
enum: [ 'draft', 'publish', 'pending' ],
},
},
required: [ 'title' ],
},
output_schema: {
type: 'object',
properties: {
id: { type: 'integer' },
link: { type: 'string' },
},
required: [ 'id' ],
},
callback: async ( input ) => {
// Implementation...
return { id: 123, link: 'https://example.com/?p=123' };
},
} );You can also validate values directly:
import { validateValueFromSchema } from '@wordpress/abilities';
const schema = {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
},
required: [ 'email' ],
};
const result = validateValueFromSchema( { email: 'not-an-email' }, schema, 'input' );
// Returns a string error message, or `true` if validAbilities can include a permissionCallback that runs before execution. If it returns false, execution is denied with an ability_permission_denied error:
registerAbility( {
name: 'my-plugin/delete-item',
label: 'Delete Item',
description: 'Deletes an item permanently',
category: 'content-management',
permissionCallback: async ( input ) => {
// Check if the current user has permission
const user = await getCurrentUser();
return user.capabilities.includes( 'delete_posts' );
},
callback: async ( input ) => {
// Deletion logic
},
} );Server-side abilities are registered in PHP and exposed via the REST API at /wp-abilities/v1/. The @wordpress/core-abilities package handles the integration:
- On page load, it fetches all categories from
GET /wp-abilities/v1/categories. - It registers each category into the client store.
- It fetches all abilities from
GET /wp-abilities/v1/abilities. - For each server ability, it creates a REST-backed callback and registers it into the client store.
HTTP method selection for server abilities is based on annotations:
| Annotations | HTTP Method | Use Case |
|---|---|---|
readonly: true |
GET |
Read-only queries |
destructive: true + idempotent: true |
DELETE |
Safe-to-retry deletions |
| Default | POST |
State-changing operations |
REST endpoints for execution:
POST /wp-abilities/v1/abilities/{name}/run — Default (state-changing)
GET /wp-abilities/v1/abilities/{name}/run — Read-only abilities
DELETE /wp-abilities/v1/abilities/{name}/run — Destructive + idempotent abilities
For GET/DELETE, input is passed as query parameters. For POST, input is sent in the request body as { input: ... }.
To use @wordpress/core-abilities, simply import it — it auto-initializes:
import '@wordpress/core-abilities';| Function | Signature | Description |
|---|---|---|
getAbilities |
( args?: { category?: string } ) => Ability[] |
Returns all abilities, optionally filtered by category. |
getAbility |
( name: string ) => Ability | undefined |
Returns a specific ability by name. |
getAbilityCategories |
() => AbilityCategory[] |
Returns all registered categories. |
getAbilityCategory |
( slug: string ) => AbilityCategory | undefined |
Returns a specific category by slug. |
registerAbility |
( ability: Ability ) => void |
Registers a client-side ability. Throws on validation failure. |
unregisterAbility |
( name: string ) => void |
Unregisters an ability. |
registerAbilityCategory |
( slug: string, args: AbilityCategoryArgs ) => void |
Registers a category. Throws on validation failure. |
unregisterAbilityCategory |
( slug: string ) => void |
Unregisters a category. |
executeAbility |
( name: string, input?: any ) => Promise<any> |
Executes an ability with optional input. |
validateValueFromSchema |
( value: any, schema: object, param?: string ) => true | string |
Validates a value against a JSON Schema. Returns true or an error message. |
interface Ability {
name: string;
label: string;
description: string;
category: string;
input_schema?: Record< string, any >;
output_schema?: Record< string, any >;
callback?: ( input: any ) => any | Promise< any >;
permissionCallback?: ( input?: any ) => boolean | Promise< boolean >;
meta?: {
annotations?: {
clientRegistered?: boolean;
serverRegistered?: boolean;
readonly?: boolean;
destructive?: boolean;
idempotent?: boolean;
};
[ key: string ]: any;
};
}
interface AbilityCategory {
slug: string;
label: string;
description: string;
meta?: {
annotations?: {
clientRegistered?: boolean;
serverRegistered?: boolean;
};
[ key: string ]: any;
};
}
interface AbilityCategoryArgs {
label: string;
description: string;
meta?: Record< string, any >;
}| Code | When |
|---|---|
ability_permission_denied |
permissionCallback returned false. |
ability_invalid_input |
Input failed schema validation. |
ability_invalid_output |
Output failed schema validation. |
The Connectors system manages API credentials for external AI service providers through a unified WordPress admin interface.
┌─────────────────────────────────────────────────────┐
│ Settings > Connectors Page │
│ (routes/connectors-home/) │
│ Renders registered connectors from store │
└──────────────────────┬──────────────────────────────┘
│ reads from
┌──────────────────────▼──────────────────────────────┐
│ @wordpress/connectors │
│ Store: core/connectors │
│ registerConnector() / ConnectorItem component │
└──────────────────────┬──────────────────────────────┘
│ saves to / reads from
┌──────────────────────▼──────────────────────────────┐
│ WordPress REST API + Options │
│ /wp/v2/settings — API key storage │
│ /wp/v2/plugins — Plugin install/activate │
└──────────────────────┬──────────────────────────────┘
│ authenticates
┌──────────────────────▼──────────────────────────────┐
│ WordPress\AiClient\AiClient │
│ Provider registry with authenticated requests │
└─────────────────────────────────────────────────────┘
Key components:
@wordpress/connectorspackage — Client-side store and UI components.routes/connectors-home/— The admin page route that renders the Connectors settings page.lib/experimental/connectors/— PHP backend for settings registration, API key masking, validation, and WP AI Client integration.
The Connectors page is accessible at Settings > Connectors in the WordPress admin. It displays all registered connectors and allows users to:
- Install a required provider plugin (if not installed).
- Activate the plugin (if installed but inactive).
- Set up the connector by entering an API key.
- Edit or remove an existing API key.
The page subtitle reads: "All of your API keys and credentials are stored here and shared across plugins. Configure once and use everywhere."
Three AI providers are registered by default:
| Connector Slug | Provider | Plugin Slug | Setting Name | Help URL |
|---|---|---|---|---|
core/openai |
OpenAI | ai-provider-for-openai |
connectors_ai_openai_api_key |
https://platform.openai.com |
core/claude |
Anthropic (Claude) | ai-provider-for-anthropic |
connectors_ai_anthropic_api_key |
https://console.anthropic.com |
core/gemini |
Google (Gemini) | ai-provider-for-google |
connectors_ai_google_api_key |
https://aistudio.google.com |
Each default connector follows a lifecycle:
- Check if the provider plugin is installed via
/wp/v2/plugins. - If not installed, show an "Install" button that installs and activates the plugin.
- If installed but inactive, show an "Activate" button.
- Once active, show a "Set up" button that reveals the API key form.
- After saving a valid key, show a "Connected" badge and an "Edit" button.
Plugins can register their own connectors to appear on the Connectors settings page:
import {
__experimentalRegisterConnector as registerConnector,
__experimentalConnectorItem as ConnectorItem,
__experimentalDefaultConnectorSettings as DefaultConnectorSettings,
} from '@wordpress/connectors';
registerConnector( 'my-plugin/my-provider', {
label: 'My AI Provider',
description: 'Custom AI capabilities powered by My Provider.',
icon: <MyProviderIcon />,
render: ( { slug, label, description } ) => (
<ConnectorItem
icon={ <MyProviderIcon /> }
name={ label }
description={ description }
>
<DefaultConnectorSettings
initialValue=""
helpUrl="https://my-provider.com/api-keys"
onSave={ async ( apiKey ) => {
// Save the API key
await saveMyApiKey( apiKey );
} }
onRemove={ async () => {
// Remove the API key
await removeMyApiKey();
} }
/>
</ConnectorItem>
),
} );Note: All Connector APIs are currently exported with the __experimental prefix, indicating they may change in future releases.
| Property | Type | Required | Description |
|---|---|---|---|
label |
string |
Yes | Display name of the connector. |
description |
string |
Yes | Short description of the provider's capabilities. |
icon |
ReactNode |
No | Icon or logo component. |
render |
( props: ConnectorRenderProps ) => ReactNode |
No | Custom render function for the connector card. |
| Property | Type | Description |
|---|---|---|
slug |
string |
The registered connector slug. |
label |
string |
The connector label. |
description |
string |
The connector description. |
The PHP backend (in lib/experimental/connectors/) handles:
API keys are stored as WordPress options, registered via register_setting():
// Settings are registered on the 'init' hook
register_setting( 'connectors', 'connectors_ai_openai_api_key', array(
'type' => 'string',
'label' => 'OpenAI API Key',
'description' => 'API key for the OpenAI AI provider.',
'default' => '',
'show_in_rest' => true,
'sanitize_callback' => $sanitize_function,
) );When API keys are read from the database, they are automatically masked to show only the last 4 characters:
Original: sk-proj-abc123def456fj39
Masked: ••••••••••••••••fj39
This is implemented via option_{$setting_name} filters that apply masking whenever the option is read.
When saving an API key, the sanitize callback validates it against the provider using the WP AI Client registry:
- The key is sanitized with
sanitize_text_field(). - The key is tested against the provider via
AiClient::defaultRegistry(). - If validation fails, an empty string is saved (rejecting the key).
The REST API also supports explicit validation when fetching settings: if a stored key fails validation, the response value is replaced with 'invalid_key'.
The Connectors system depends on WordPress core's \WordPress\AiClient\AiClient class. This dependency is checked before any connector functionality loads:
// In lib/load.php
if ( class_exists( '\WordPress\AiClient\AiClient' ) ) {
require __DIR__ . '/experimental/connectors/load.php';
}On the init hook, stored API keys are passed to the AI Client registry:
$registry = \WordPress\AiClient\AiClient::defaultRegistry();
$registry->setProviderRequestAuthentication(
$provider_id,
new \WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication( $api_key )
);This means any plugin or theme that uses the WP AI Client can make authenticated requests to configured providers without needing to manage API keys itself.
Exports (all __experimental prefixed):
| Export | Type | Description |
|---|---|---|
__experimentalRegisterConnector |
function |
Register a connector for the settings page. |
__experimentalConnectorItem |
component |
UI component for rendering a connector card. |
__experimentalDefaultConnectorSettings |
component |
Default API key input form with save/remove. |
| Prop | Type | Required | Description |
|---|---|---|---|
icon |
ReactNode |
No | Provider logo/icon. |
name |
string |
Yes | Display name. |
description |
string |
Yes | Short description. |
actionArea |
ReactNode |
No | Buttons/badges in the card header. |
children |
ReactNode |
No | Expanded content (e.g. settings form). |
| Prop | Type | Default | Description |
|---|---|---|---|
initialValue |
string |
'' |
Pre-populated API key value. |
helpUrl |
string |
— | URL to the provider's API key page. |
helpLabel |
string |
Derived from URL | Custom label for the help link. |
readOnly |
boolean |
false |
Whether the key is in read-only mode (already saved). |
onSave |
( apiKey: string ) => void | Promise<void> |
— | Called when the user clicks Save. |
onRemove |
() => void |
— | Called when the user clicks Remove. |
The store uses private APIs (accessed via unlock()):
| Selector | Returns | Description |
|---|---|---|
getConnectors() |
ConnectorConfig[] |
All registered connectors. |
getConnector( slug ) |
ConnectorConfig | undefined |
A specific connector. |
| Action | Description |
|---|---|
registerConnector( slug, config ) |
Register a new connector. |
| Function | Description |
|---|---|
_gutenberg_mask_api_key( $key ) |
Masks an API key, showing only the last 4 characters. |
_gutenberg_is_api_key_valid( $key, $provider_id ) |
Validates an API key against a provider. Returns true, false, or null. |
_gutenberg_get_provider_settings() |
Returns the configuration for all default connector providers. |
_gutenberg_register_default_connector_settings() |
Registers WordPress settings and filters for connector API keys. |
_gutenberg_pass_default_connector_keys_to_ai_client() |
Passes stored API keys to the WP AI Client registry. |
_gutenberg_validate_connector_keys_in_rest() |
Validates API keys in REST settings responses when explicitly requested. |
Note: Functions prefixed with _gutenberg_ are private/internal and will likely be renamed when merged into WordPress core.
The Abilities API and Connectors form a complete AI integration framework:
-
Connectors handle authentication — users configure API keys for AI providers once, and those credentials are securely stored and shared across all plugins.
-
The WP AI Client (WordPress core) uses those credentials to make authenticated requests to AI providers.
-
Abilities define what WordPress can do — plugins register discoverable, validated actions that AI assistants (or other consumers) can find and execute.
-
AI assistants can query the Abilities registry to discover available actions, understand their input/output contracts, and execute them — all through a standardized API.
Example flow:
User configures OpenAI API key in Settings > Connectors
↓
Key is stored in wp_options and passed to WP AI Client registry
↓
A plugin registers a "generate-alt-text" ability (server-side)
↓
An AI assistant discovers the ability via GET /wp-abilities/v1/abilities
↓
The assistant calls POST /wp-abilities/v1/abilities/my-plugin/generate-alt-text/run
↓
The server-side ability uses the WP AI Client (with the stored OpenAI key)
to generate alt text and returns the result
This separation of concerns means plugin developers don't need to build their own credential management UI, and AI consumers have a standardized way to discover and invoke WordPress capabilities.