wp_register_ability( string $name, array $args ): ?WP_Ability returns null on
failure, and by design it does this quietly. There’s no exception, no fatal error, your
plugin just keeps running as if nothing happened, until you notice the ability isn’t in
wp_get_abilities() and have to work backward to find out why. This lesson covers the
real, documented causes, confirmed against WordPress core’s own Abilities API
documentation and against real, closed reports against the MCP Adapter, and how to
catch each one immediately instead of discovering it later.
The general debugging workflow from Lesson 1. WP_DEBUG and WP_DEBUG_LOG enabled on your local environment, since several of the failures in this lesson only surface a notice when debugging is on.
Step 1: Registering outside wp_abilities_api_init
Every ability must be registered inside the wp_abilities_api_init action. Calling
wp_register_ability() directly on init, on plugins_loaded, or at the top level of
a plugin file with no hook at all, is the single most common cause of a null return.
WordPress core’s own documentation is explicit about this: abilities must be
registered during the wp_abilities_api_init action, and calling the function from
init, plugins_loaded, or any other hook triggers a _doing_it_wrong() notice and
the registration is discarded.
// File: my-plugin.php, wrong
add_action( 'init', function () {
wp_register_ability( 'my-plugin/my-ability', array( /* ... */ ) ); // returns null
} );
// File: my-plugin.php, correct
add_action( 'wp_abilities_api_init', function () {
wp_register_ability( 'my-plugin/my-ability', array( /* ... */ ) );
} );
This is the trap that wastes the most time. The _doing_it_wrong() notice core emits
for wrong-hook registration only appears in wp-content/debug.log when WP_DEBUG and
WP_DEBUG_LOG are both enabled. On a site with debugging off, you get nothing at all,
just a null return and an ability that never shows up. Always confirm debug logging
is on before concluding a registration “fails for no reason.”
Step 2: The one-shot hook window, registering too late
Because wp_abilities_api_init fires once per request, there’s a second, less obvious
version of the same problem: your add_action( 'wp_abilities_api_init', ... ) call
itself has to be attached before WordPress fires that action. If your plugin’s
bootstrap defers attaching that callback behind something that only runs later in the
request (a conditional deferred to a late-priority init callback, or logic gated
behind another plugin’s own late-firing hook), the action can already have fired by the
time your callback is attached, and it simply never runs, WordPress does not replay
hooks for late listeners. Community members debugging real registration failures have
described this exact symptom: wp_register_ability() returning null with an
invisible _doing_it_wrong() notice, because the one-shot registration window had
already closed by the time the call happened.
// File: my-plugin.php, risky, deferred too far
add_action( 'init', function () {
add_action( 'wp_abilities_api_init', 'my_plugin_register_abilities' );
}, 999 ); // priority 999 on init may already be too late in some load orders
Attach your wp_abilities_api_init listener directly, at your plugin’s top level or on
a normal-priority plugins_loaded, not nested behind another late hook:
// File: my-plugin.php, correct
add_action( 'wp_abilities_api_init', 'my_plugin_register_abilities' );
function my_plugin_register_abilities() {
wp_register_ability( 'my-plugin/my-ability', array( /* ... */ ) );
}
Step 3: Duplicate ability names
The registry rejects a second registration under a name that’s already taken. Two
plugins registering analytics/get-report, or your own plugin accidentally calling
wp_register_ability() twice for the same name (easy to do if a class constructor runs
more than once, or if you register the same ability from two different files by
mistake), both produce the same result: the second call returns null.
# File: terminal, checking for a naming collision
wp eval '
$abilities = wp_get_abilities();
echo isset( $abilities["my-plugin/my-ability"] ) ? "already registered" : "not registered";
'
This is the same discipline Course 1 covers for naming abilities in the first place.
my-ability with no plugin prefix is a collision waiting to happen the moment a second
plugin picks the same generic name. Always namespace to your own plugin or theme slug.
Step 4: Missing required arguments
wp_register_ability()’s $args array has required keys: label, description,
category, execute_callback, and permission_callback. input_schema and
output_schema are optional. Omitting a required field, or passing a category slug
that hasn’t itself been registered yet, produces the same silent null. The next
lesson is entirely about the most common, most confusing version of this specific
failure: a missing category, and the real GitHub issue where it took the reporting
developer, and several other people, a while to trace it back to something that simple.
Test it: a registration health check you can rerun
# File: terminal
wp eval '
$name = "my-plugin/my-ability";
$result = wp_has_ability( $name );
echo "Registered: " . ( $result ? "yes" : "NO, check hook timing, duplicates, and required args" ) . PHP_EOL;
'
Then check the log for anything WordPress tried to tell you and you missed:
# File: terminal
grep -i "doing_it_wrong" wp-content/debug.log | tail -n 5
Recap
wp_register_ability() returns null on failure, silently by design. The real causes
are registering outside wp_abilities_api_init (including attaching that hook itself
too late in the request), a duplicate ability name already in the registry, and a
missing or invalid required argument, most often a category that either wasn’t
supplied or wasn’t registered yet. _doing_it_wrong() notices exist for most of these,
but they’re invisible without WP_DEBUG and WP_DEBUG_LOG turned on. The next lesson
walks through the specific, real GitHub issue where a missing category was mistaken
for something far more exotic.
Resources & further reading
- wp_register_ability() function reference, developer.wordpress.org
- Abilities API PHP reference, GitHub
- Abilities API reference, developer.wordpress.org
- MCP Adapter repository, GitHub