Skip to content

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.

As of 1.5.0, 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 70 routes / 111 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.

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' ),
) );
}

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.

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.

  • 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