WordPress 7.0 introduced the Abilities API — a structured registry that lets plugins declare what they can do so that AI agents, browser extensions, and WebMCP integrations can discover and execute those actions programmatically.
ClientPress implements this API. This document explains the architecture, lists every registered ability, describes what end users can do with it today, and lays out how to extend it in the future.
How It Works #
The two-package model #
WordPress ships two JavaScript packages:
@wordpress/abilities— Pure state management. Provides the store,registerAbility,executeAbility, etc. No WordPress server dependency.@wordpress/core-abilities— WordPress integration layer. When loaded, auto-fetches all server-registered abilities via/wp-abilities/v1/and registers them in the store. Core enqueues this on every admin page.
Because WordPress core enqueues @wordpress/core-abilities on all admin pages, every ability ClientPress registers on the PHP side is automatically available to any AI agent or browser extension that queries the abilities store — no additional client-side setup required.
How server-side abilities work #
- ClientPress calls
wp_register_ability_category()andwp_register_ability()on theinithook. - WordPress exposes these over the REST endpoint
/wp-abilities/v1/. - Any client that loads
@wordpress/core-abilitiesfetches that endpoint and hydrates the store. - When a client calls
executeAbility('clientpress/create-portal', { title: 'Acme' }), WordPress routes it back to the server via the REST API and calls our PHP callback. - Our callback runs, creates the portal, fires the activity log and webhook, and returns a typed output.
How client-side abilities work #
ClientPress also enqueues assets/js/cp-abilities.js as an ES module on every admin page. This module imports from @wordpress/abilities and registers navigation abilities that only exist in the browser — no server round-trip needed.
Backward compatibility #
Both the PHP ability registration and the JS module enqueue are guarded:
// PHP side
if ( ! function_exists( 'wp_register_ability_category' ) ) {
return;
}
// JS side
if ( ! function_exists( 'wp_enqueue_script_module' ) ) {
return;
}
On WordPress versions before 7.0, the entire feature silently no-ops. No errors, no broken admin pages.
Registered Abilities #
All server-side abilities require the executing user to have manage_options. Validation against input_schema and output_schema is handled automatically by WordPress before and after the callback runs.
Category: clientpress-portals #
clientpress/create-portal #
Creates a new client portal.
title(string, required) — Portal titlestatus(active|pending|archived) — Defaults toactiveaccent_color(string) — Hex colour, e.g.#3b82f6allow_uploads(boolean) — Defaults totrueclient_user_id(integer) — WP user ID of the primary clienttemplate_id(integer) — Portal template ID to apply on creation
Output: { id, title, url }
Fires: ActivityLog::PORTAL_CREATED, portal.created webhook.
clientpress/update-portal #
Updates an existing portal’s settings.
portal_id(integer, required) — Portal post IDstatus(active|pending|archived)accent_color(string) — Hex colourallow_uploads(boolean)
Output: { id, success }
clientpress/invite-client #
Sends a client invitation email and creates a time-limited signup link.
portal_id(integer, required) — Portal post IDemail(string/email, required) — Client email addressname(string) — Client display name
Output: { success, message }
Delegates to Invitations::send_invite() — identical to sending an invite from the portal editor UI.
clientpress/assign-client #
Assigns an existing WordPress user to a portal. If no primary client is set, the user becomes primary; otherwise they are added as a sub-client.
portal_id(integer, required) — Portal post IDuser_id(integer, required) — WP user ID to assign
Output: { portal_id, role } where role is primary or sub-client.
clientpress/create-hub #
Creates a child portal (hub) nested under a parent portal.
title(string, required) — Hub titleparent_portal_id(integer, required) — Parent portal post IDstatus(active|pending|archived) — Defaults toactive
Output: { id, title, url }
Category: clientpress-tasks #
Only registered when Settings → Task Manager is enabled.
clientpress/create-task #
Creates a new task in a portal task list.
portal_id(integer, required) — Portal post IDlist_id(integer, required) — Task list IDtitle(string, required) — Task titleassignee_ids(integer[]) — WP user IDs to assign
Output: { id, title }
Fires: ActivityLog::TASK_CREATED, task.created webhook, task assignment notification emails.
clientpress/update-task-status #
Changes the status of a task.
task_id(integer, required) — Task IDstatus(open|up_next|in_progress|complete, required) — New status
Output: { task_id, status }
Fires: TASK_COMPLETED, TASK_REOPENED, or TASK_STATUS_CHANGED depending on the new status. Fires task.completed webhook when status is complete.
Category: clientpress-files #
clientpress/approve-file #
Approves a pending file upload.
portal_id(integer, required) — Portal post IDfilename(string, required) — Internal filename key (from the_cp_filesrecord’snamefield)
Output: { success, approved_at }
Fires: ActivityLog::FILE_APPROVED, approval notification email to uploader, file.approved webhook.
clientpress/reject-file #
Rejects a pending file upload.
portal_id(integer, required) — Portal post IDfilename(string, required) — Internal filename keynote(string) — Rejection reason shown to the uploader
Output: { success, rejected_at }
Fires: ActivityLog::FILE_REJECTED, rejection notification email to uploader, file.rejected webhook.
Category: clientpress-navigate (client-side only) #
These abilities are registered in JavaScript and only perform in-browser navigation. They appear in the WordPress command palette and are available to browser agents. They do not make server round-trips.
clientpress/navigate-to-portal— Opens a portal’s edit screen. Input:{ portal_id }clientpress/navigate-to-settings— Opens the ClientPress settings pageclientpress/navigate-to-invitations— Opens the invitations management page
All navigation abilities carry meta.annotations.readonly: true so WordPress routes execution as GET.
What Users Can Do With This Today #
Once on WordPress 7.0, any AI agent or automation tool that understands the Abilities API can interact with ClientPress without custom integration code. Practical examples:
Onboard a new client in one step
“Create a portal for Acme Corp, apply the Standard Onboarding template, and send an invite to jane@acme.com.”
The agent calls clientpress/create-portal with the template, then clientpress/invite-client — two ability calls, no clicking.
Automate project kickoff
“When a new deal is marked Won in my CRM, create a portal, create a ‘Project Kickoff’ task list with three tasks, and assign them to my team.”
A workflow tool can chain clientpress/create-portal → clientpress/create-task × 3 using data from the CRM trigger.
Archive completed engagements
“Set all portals tagged ‘Q1 2026’ to Archived.”
The agent queries portals via GET /cp/v1/portals, then calls clientpress/update-portal for each one.
Command palette shortcuts Admins can type “Navigate to portal” or “Go to Invitations” in the WordPress command palette and jump directly — no mouse required.
Browser agent automation Browser extensions built on WebMCP can list available ClientPress abilities, prompt the user to confirm, and execute them — all through the standard Abilities API interface.
Architecture Notes #
Audit trail parity #
Every server-side ability callback fires the same activity log entries, notification emails, and outbound webhooks as the equivalent AJAX or REST API action. An AI agent creating a portal is indistinguishable from an admin doing it manually in the audit trail. This is intentional — it means existing monitoring and webhook consumers work without changes.
Abilities vs the existing REST API #
The REST API (cp/v1) and the Abilities API serve different purposes and are both kept:
- REST API (
cp/v1) — Primary consumer: OttoKit, Zapier, n8n, custom code. Auth: Application Passwords or API key. Discovery: manual (docs). Schema validation: per-endpointargs. Navigation: not applicable. - Abilities API — Primary consumer: AI agents, browser extensions, WP command palette. Auth: WordPress session (same user doing the action). Discovery: automatic via
/wp-abilities/v1/. Schema validation: JSON Schema draft-04, automatic. Navigation: supported (client-side abilities).
Why callbacks don’t call the REST handlers #
The REST handlers (RestAPI::create_portal(), etc.) accept \WP_REST_Request objects and return \WP_REST_Response. Ability callbacks accept plain arrays and return plain arrays or \WP_Error. Rather than wrapping one format in the other, the callbacks call the same underlying WordPress and ClientPress functions directly (wp_insert_post, update_post_meta, Invitations::send_invite, Tasks::create, etc.). If shared logic grows complex enough to warrant extraction, a PortalService or similar class would be the right place to put it.
Extending in the Future #
Adding a new ability #
- Register a new
wp_register_ability()call inAbilities::register_server_abilities(). - Add a static callback method on the
Abilitiesclass. - Define
input_schemaandoutput_schemausing JSON Schema draft-04. - Call any necessary activity log, webhook, and notification methods from the callback — same as you would in a REST handler or AJAX handler.
Example skeleton:
wp_register_ability( [
'name' => 'clientpress/archive-portal',
'label' => __( 'Archive Portal', 'clientpress' ),
'description' => __( 'Moves a portal to Archived status', 'clientpress' ),
'category' => 'clientpress-portals',
'permission_callback' => fn() => current_user_can( 'manage_options' ),
'meta' => [ 'annotations' => [ 'destructive' => true, 'idempotent' => true ] ],
'input_schema' => [
'type' => 'object',
'properties' => [
'portal_id' => [ 'type' => 'integer' ],
],
'required' => [ 'portal_id' ],
],
'output_schema' => [
'type' => 'object',
'properties' => [
'success' => [ 'type' => 'boolean' ],
],
'required' => [ 'success' ],
],
'callback' => [ self::class, 'cb_archive_portal' ],
] );
Abilities worth adding next #
clientpress/get-portal-activity— Read-only; useful for agents that summarise what happened in a portal. Annotatereadonly: true.clientpress/create-task-list— Agents can’t currently create a list to put tasks in — they can only add to existing ones.clientpress/delete-portal— Destructive; annotatedestructive: true. Gate carefully.clientpress/search-portals— Lets agents find a portal by name before acting on it. Annotatereadonly: true.clientpress/get-pending-files— Let agents surface a summary of files awaiting approval across all portals.
Adding client-side abilities #
Client-side abilities go in assets/js/cp-abilities.js. Import from @wordpress/abilities and call registerAbility. These are good for:
- Navigation shortcuts (already done)
- UI state changes that don’t need a server round-trip
- Triggering existing portal JavaScript actions (e.g. opening the invite modal)
Allowing clients (not just admins) to use abilities #
Currently all server-side abilities require manage_options. If a future version wants to expose abilities to portal clients — e.g. clientpress/upload-file or clientpress/post-message — the permission_callback should use PostTypes::current_user_can_view( $portal_id ) instead. Be deliberate about which actions clients should be able to trigger from an AI agent vs. only from the portal UI.
Exposing abilities conditionally by feature flag #
The task abilities already demonstrate this pattern — they only register when tasks_enabled is on. Apply the same pattern to any future ability that depends on a feature toggle:
if ( Admin\Settings::get()['discussions_enabled'] ) {
wp_register_ability( [ 'name' => 'clientpress/post-message', ... ] );
}
