Abilities API
WordPress 7.0 ships the Abilities API - a registry that lets a plugin declare
discrete, machine-callable actions (wp_register_ability()), each with a JSON
schema, a permission callback, and annotations describing whether it reads or
writes. Registered abilities are exposed over /wp-abilities/v1 and can be
surfaced to MCP clients and AI agents.
Current State: Eventonomy Registers None
As of 1.3.1, Eventonomy and Eventonomy Pro register zero abilities. There is no
wp_register_ability() call in either repository. Verified on a WordPress 7.0
install: wp_get_abilities() returns 10, of which 3 are core and the rest belong to
WooCommerce - none are ours.
This page documents that honestly rather than describing a surface that does not
exist. If you are looking for a machine-callable interface to Eventonomy today,
use the REST API (reference) - it is complete, versioned, and
covers 67 routes / 109 operations with both plugins active (the count the generated
docs/api/openapi.json reports).
Why this matters. Eventonomy is REST-first with no AJAX and a documented envelope, so it is unusually well-placed to expose abilities - the hard part (a clean, permission-checked action surface) is already built. WooCommerce has already adopted the API. Treat this as a known gap, not a design decision.
Registering Abilities Against Eventonomy
Nothing stops you registering your own today. The plugin's service layer and capability helpers are the right things to call.
add_action( 'wp_abilities_api_init', function () {
if ( ! function_exists( 'wp_register_ability' ) ) {
return; // WordPress < 7.0
}
wp_register_ability(
'my-addon/find-events',
array(
'label' => __( 'Find events', 'my-addon' ),
'description' => __( 'Search upcoming events by text, city or date range.', 'my-addon' ),
'category' => 'my-addon',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'search' => array( 'type' => 'string' ),
'city' => array( 'type' => 'string' ),
'from' => array( 'type' => 'string', 'format' => 'date-time' ),
),
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'items' => array( 'type' => 'array' ),
'total' => array( 'type' => 'integer' ),
),
),
// Go through the contract, never raw SQL.
'execute_callback' => function ( array $input ) {
$events = evnm( \Eventonomy\Contracts\EventRepositoryInterface::class );
return $events->query( array(
'search' => $input['search'] ?? '',
'city' => $input['city'] ?? '',
'status' => 'published',
'per_page' => 20,
) );
},
// Reuse Eventonomy's resolution, do not re-implement it.
'permission_callback' => function (): bool {
return true; // published events are public
},
'meta' => array(
'annotations' => array(
'readonly' => true,
'destructive'=> false,
'idempotent' => true,
),
'show_in_rest' => true,
'mcp' => array( 'public' => true, 'type' => 'tool' ),
),
)
);
} );
Register a category once, guarding for older WordPress:
if ( function_exists( 'wp_register_ability_category' ) && ! wp_has_ability_category( 'my-addon' ) ) {
wp_register_ability_category( 'my-addon', array(
'label' => __( 'My Add-on', 'my-addon' ),
'description' => __( 'Event operations provided by My Add-on.', 'my-addon' ),
) );
}
The Rules That Matter Most
1. The permission callback is the whole security boundary. An ability is
callable by an agent. Do not write a bare current_user_can( 'read' ) for anything
event-scoped - use the resolved helpers, which handle ownership:
'permission_callback' => function ( array $input ): bool {
$events = evnm( \Eventonomy\Contracts\EventRepositoryInterface::class );
$event = $events->get( (int) ( $input['event_id'] ?? 0 ) );
return is_array( $event )
&& evnm_user_can_manage_event( get_current_user_id(), $event, 'manage_rsvps' );
},
See Capabilities. The ownership branch is why a member organizer can manage their own event without site-manager rights - re-implementing the check will get that wrong.
2. Annotate honestly. readonly, destructive and idempotent are how a
client decides whether it may call something unattended, retry it, or must ask a
human first. Marking a refund idempotent: true because "the service is
idempotent" invites an agent to retry a money operation. If in doubt, mark it
destructive: true and let the client ask.
3. Never expose a secret through an ability. The same fields withheld from REST
are withheld here: magic_token, checkin_token, gateway_txn_id, and the order
meta blob. An ability is not a privileged back door.
4. Money and attendance are not read operations. Anything that creates an RSVP,
mints a ticket, refunds an order or records a payout must go through the service
layer (RsvpService, OrderService, Pro's RefundService / PayoutService) so
the hooks, ledgers and reversals fire. Writing to a repository directly from an
ability skips all of that.
If Eventonomy Adopts This
The natural first set, in the order the existing REST surface makes them cheap - recorded here so the shape is agreed before anyone builds it:
| Ability | Annotations | Backed by |
|---|---|---|
eventonomy/find-events |
readonly, idempotent | EventRepositoryInterface::query() |
eventonomy/get-event |
readonly, idempotent | EventRepositoryInterface::get() |
eventonomy/create-event |
write, not idempotent | EventService::create() |
eventonomy/list-attendees |
readonly, idempotent | RsvpRepositoryInterface::query() |
eventonomy/check-in-attendee |
write, idempotent | Pro CheckinService (already idempotent + void-gated) |
eventonomy/refund-order |
write, destructive | Pro RefundService - full order only |
Reads should come first: they are safe, immediately useful to an assistant answering "what is on next week", and they exercise the registration plumbing before anything can move money.
What's Next?
- REST API - the complete machine interface that exists today
- Capabilities - the permission model an ability must reuse
- Extending - the service and repository contracts to call