Eventonomy

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