Core Concepts

Understanding Abilities: WordPress's New Way to Expose Site Actions

⏱ 12 min

An ability is WordPress core’s standard shape for “a thing this site can do, described precisely enough that code you didn’t write can call it safely.” This lesson looks at that shape closely, without yet writing a real plugin, that’s Course 2. For now, the goal is recognizing every part of an ability registration and knowing what each part is for.

What you'll learn in this lesson
The anatomy of wp_register_ability()
Every argument it accepts and what each one controls.
Why abilities are namespaced
How naming prevents collisions between plugins.
The other Abilities API functions
wp_get_ability(), wp_has_ability(), wp_get_abilities(), wp_unregister_ability().
Categories
Why abilities are grouped, and how that grouping is registered.
Prerequisites

A local WordPress 6.9+ environment (from the previous lesson). You’ll read code in this lesson but won’t need to run any of it yet.

Step 1: The shape of wp_register_ability()

Here’s a minimal, illustrative registration (you’ll build a real one in Course 2):

// File: my-plugin.php
add_action( 'wp_abilities_api_init', function () {
	wp_register_ability(
		'my-plugin/get-latest-post-title',
		array(
			'label'             => __( 'Get latest post title', 'my-plugin' ),
			'description'       => __( 'Returns the title of the most recently published post.', 'my-plugin' ),
			'category'          => 'content',
			'input_schema'      => array( 'type' => 'object', 'properties' => array() ),
			'output_schema'     => array(
				'type'       => 'object',
				'properties' => array( 'title' => array( 'type' => 'string' ) ),
			),
			'permission_callback' => function () {
				return current_user_can( 'edit_posts' );
			},
			'execute_callback'    => function () {
				$latest = get_posts( array( 'numberposts' => 1 ) );
				return array( 'title' => $latest ? $latest[0]->post_title : '' );
			},
			'meta' => array(
				'annotations' => array( 'readonly' => true ),
				'mcp'         => array( 'public' => false ),
			),
		)
	);
} );

Every ability is always registered inside the wp_abilities_api_init action, this is the one hook the Abilities API fires specifically for registration, similar in purpose to how blocks are registered on init.

Step 2: Every field, explained

wp_register_ability() arguments
FieldPurpose
labelShort, human-readable name shown to anyone (or any AI client) browsing available abilities.
descriptionLonger explanation. This is what an AI model actually reads to decide whether to use this ability, write it for a model, not just a human.
categoryGroups related abilities together, registered separately via wp_register_ability_category().
input_schema / output_schemaStandard JSON Schema describing exactly what arguments the ability accepts and what shape it returns.
permission_callbackA function returning true/false. Runs before execute_callback, no permission, no execution.
execute_callbackThe actual PHP that runs when the ability is invoked.
meta.annotationsHints about the ability’s behavior: readonly, destructive, idempotent. Course 2 covers how these map to MCP tool hints.
meta.mcp.publicWhether the MCP Adapter is allowed to expose this ability to external AI clients at all. Covered in full in Lesson 6.

Step 3: Why the namespaced name matters

The first argument, 'my-plugin/get-latest-post-title', isn’t just a label, it’s the ability’s permanent identifier. Namespacing it to your plugin slug (my-plugin/...) prevents two plugins from registering an ability with the same name and silently overwriting each other. Treat it the same way you’d treat a function name in a namespace, stable, and never reused for something unrelated later.

Step 4: Reading and checking abilities elsewhere in your code

Beyond registering, the Abilities API gives you functions to introspect what’s registered, useful in your own code even with no AI client involved at all:

// Check an ability exists before relying on it
if ( wp_has_ability( 'my-plugin/get-latest-post-title' ) ) {
	$ability = wp_get_ability( 'my-plugin/get-latest-post-title' );
	$result  = $ability->execute( array() );
}

// List everything registered
$all = wp_get_abilities();

wp_unregister_ability() exists too, for plugins that need to conditionally remove one, for example if a dependent feature gets disabled.

Test it: register and call your own ability

Add the code from Step 1 to a plugin file (or a must-use plugin) on your local environment, activate it, then confirm it registered:

wp-env run cli wp eval 'var_dump( wp_has_ability( "my-plugin/get-latest-post-title" ) );'

You should see bool(true).

Forgetting the action, or using the wrong one

Registering outside wp_abilities_api_init (for example directly on init) can run before the Abilities API’s own internals are ready, causing a fatal error on some WordPress versions. Always use wp_abilities_api_init, and register categories on wp_abilities_api_categories_init if you’re defining your own category rather than reusing an existing one.

Recap

An ability is a namespaced, schema-described, permission-checked unit of functionality, registered on wp_abilities_api_init with wp_register_ability(). The permission_callback and execute_callback pair is the security-critical part, and meta.mcp.public is the separate, explicit gate for whether this ability is even reachable by an external AI client, a distinction the next few lessons build on.

Resources & further reading

← Setting Up Your Local WordPress AI Development Environment MCP Core Concepts: Tools, Resources, and Prompts →