Skip to content

Utilize the new client-side Abilities API#482

Open
dkotter wants to merge 10 commits intoWordPress:developfrom
dkotter:feature/client-side-ability-api
Open

Utilize the new client-side Abilities API#482
dkotter wants to merge 10 commits intoWordPress:developfrom
dkotter:feature/client-side-ability-api

Conversation

@dkotter
Copy link
Copy Markdown
Collaborator

@dkotter dkotter commented Apr 28, 2026

Important

This relies on a new version of WordPress 7.0 being shipped that includes https://core.trac.wordpress.org/ticket/65035. Marking this as blocked until then.

What?

Closes #346

Ensure we properly use the new client-side Abilities API

Why?

We currently have a helper method that attempted to use the client-side Abilities API, falling back to making direct API requests if it wasn't available. This was based on the older client-side API and so needed updating with the new version that is shipping with WordPress 7.0 (see post).

Note this code was pulled directly from #364, as that PR is no split into this one and #481

How?

  • Update our runAbility helper function to use the new client-side Abilities API
  • Ensure the @wordpress/core-abilities script module is loaded anytime an Ability needs it
  • Fix some schema validations issues, particularly ensuring block content is always returned as a string and numbers returned as integers. Otherwise the client-side validation fails and it falls back to making direct API requests (which do work but isn't what we want here)

Use of AI Tools

AI assistance: Yes
Tool(s): Cursor
Model(s): GPT-5.3 Codex
Used for: Determining how to properly load the @wordpress/core-abilities script module based on our current setup. Also used it to debug schema validation errors. Most work was then done by me

Testing Instructions

  1. Pull this PR down and run npm i && npm run build
  2. For each feature that utilizes AI (everything besides the Abilities Explorer), turn those on one-by-one
  3. Test the feature you turned on, ensuring you have the browser console open. The feature should function the same as it always has and you shouldn't see any errors or messages in the console. Importantly, shouldn't see [AI] Client ability execution unavailable. Falling back to REST.
  4. Continuing testing each feature ensuring they all still work as expected

Changelog Entry

Fixed - Ensure we properly use the new client-side Abilities API

Open WordPress Playground Preview

@dkotter dkotter added this to the 1.0.0 milestone Apr 28, 2026
@dkotter dkotter self-assigned this Apr 28, 2026
@dkotter dkotter added the [Status] Blocked Used to indicate unable to move forward label Apr 28, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: dkotter <[email protected]>
Co-authored-by: gziolo <[email protected]>
Co-authored-by: sirreal <[email protected]>
Co-authored-by: jorgefilipecosta <[email protected]>
Co-authored-by: jeffpaul <[email protected]>
Co-authored-by: justlevine <[email protected]>
Co-authored-by: ocean90 <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 44.44444% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.19%. Comparing base (fe74e1f) to head (a04def7).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
...iments/Alt_Text_Generation/Alt_Text_Generation.php 66.66% 1 Missing ⚠️
.../Content_Classification/Content_Classification.php 0.00% 1 Missing ⚠️
.../Experiments/Content_Resizing/Content_Resizing.php 0.00% 1 Missing ⚠️
...eriments/Excerpt_Generation/Excerpt_Generation.php 0.00% 1 Missing ⚠️
.../Experiments/Meta_Description/Meta_Description.php 0.00% 1 Missing ⚠️
includes/Experiments/Refine_Notes/Refine_Notes.php 0.00% 1 Missing ⚠️
includes/Experiments/Review_Notes/Review_Notes.php 0.00% 1 Missing ⚠️
...cludes/Experiments/Summarization/Summarization.php 0.00% 1 Missing ⚠️
.../Experiments/Title_Generation/Title_Generation.php 0.00% 1 Missing ⚠️
...des/Features/Image_Generation/Image_Generation.php 0.00% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             develop     #482      +/-   ##
=============================================
+ Coverage      69.16%   69.19%   +0.02%     
- Complexity       981      982       +1     
=============================================
  Files             63       63              
  Lines           4648     4652       +4     
=============================================
+ Hits            3215     3219       +4     
  Misses          1433     1433              
Flag Coverage Δ
unit 69.19% <44.44%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

return;
}

wp_enqueue_script_module( '@wordpress/core-abilities' );
Copy link
Copy Markdown
Member

@gziolo gziolo Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity, can the build tool detect this script module dependency and automatically include it in the asset file for the entry point that needs it? It would be a more future-proof approach rather than remembering to add it manually.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll admit my knowledge on this is limited so I could be wrong here but my understanding is that since this is a script module and not a classic script, dependency extraction doesn't work here. I think we'd have to move away from using wp_enqueue_script for all of our scripts and convert those all into modules we import via wp_enqueue_script_module, along with some other changes. So this seemed like the easiest approach.

But if I'm wrong on this and there's a better approach, definitely interested in learning that.

One thing we could do that I had considered is update our Asset_Loader::enqueue_script method to auto include this script module when needed. That would reduce the duplication here and new experiments in the future wouldn't need to have this line added. Would just need an extra argument added to Asset_Loader::enqueue_script to determine if this module should be enqueued or not

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sirreal, would you mind sharing insights what’s possible with WordPress 7.0 as minimum version in regards to mixing scripts and script modules?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn’t a blocker. I’m not even sure we can do much about it without larger changes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset 61587 landed in WordPress to allow Classic Scripts to depend on Script Modules.

Usage looks like this:

wp_register_script_module( '@example/module', /*…*/ );

wp_enqueue_script(
	'example-classic',
	'/classic.js',
	array( 'classic-script-dependency' ), // Classic script dependencies.
	null,
	array(
		// Script module dependencies are declared here.
		'module_dependencies' => array( '@example/module' ),
	)
);

There's a related issue about updating build tooling:

WordPress/gutenberg#75196

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was the error you saw like this?

TypeError: Failed to resolve module specifier '@wordpress/core-abilities'

I get an error around not being able to find the Ability, example Error: Ability not found: ai/title-generation. This goes away once I add wp_enqueue_script_module( '@wordpress/core-abilities' )

Is there a compelling reason not to use modules more broadly in this project? Module->module dependencies should work without issue.

Just not the way things have been setup before this. I'm not opposed to moving that direction but that's likely a larger discussion and task.

For the script that depends on the abilities module, adding defer is a workaround that should resolve the issue.

We do already set 'strategy' => 'defer'. I added 'in_footer' => true to test but that doesn't fix things either.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to work, although there are some issues with it:

wp_register_script(
  'example',
  false,
  array( 'wp-api-fetch', 'wp-url', 'wp-data', 'wp-i18n', ),
  false,
  array(
    'in_footer' => true,
    'strategy' => 'defer',
    'module_dependencies' => array( '@wordpress/abilities', '@wordpress/core-abilities' ),
  ),
);
wp_enqueue_script( 'example' );
wp_add_inline_script(
  'example',
  <<<JAVASCRIPT
  ( async () => {
    await import( '@wordpress/core-abilities' );
    const m = await import( '@wordpress/abilities' );
    window.m = m;
    console.log({m});
  } )()
  JAVASCRIPT,
);

It seems like the abilities modules expect several classic WordPress scripts to be present (see the dependency array in the example script).

Calling the @wordpress/abilities module's getAbilities() function is an array with 2 entries. That may be because I don't have this plugin registered? Can you provide details about the steps to discover the issue you're seeing?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the steps I'm following:

  1. Checkout this PR and run npm i && npm run build
  2. Comment out this line so the script module isn't loaded: https://github.com/WordPress/ai/pull/482/changes#diff-9b683a19a4d64a079b42ade32de8038f235d7b12f619ae623796f7a9eff667cdR69
  3. Go to Settings > AI and turn on Title Generation (you don't even need an AI Connector setup to test this)
  4. Go to a post, open the browser console, click into the title and click the Regenerate button that shows
  5. You should see an error in the console: Error: Ability not found: ai/title-generation. Right after that, should see a message: [AI] Client ability execution is unavailable. Falling back to REST.. If using the client-side API doesn't work, we fallback to making direct REST requests using apiFetch
  6. Uncomment out the script module line above and try the steps again
  7. This time you should see an error around invalid schema due to sanitize_callback, which will be addressed when the next beta version of 7.0 is released. So it tried to use the executeAbility function but that failed due to schema validation issues and it then falls back to using apiFetch again

Looking at your code example, there are some differences to what I've been trying. I've only been including @wordpress/core-abilities as a script module and only been importing @wordpress/abilities. I tried adjusting that to include both but still get the same error.

I even replaced my code with the following (adjusted slightly from your example) and got the same error (if I comment out wp_enqueue_script_module( '@wordpress/core-abilities' )):

wp_register_script(
  'example',
  false,
  array( 'wp-api-fetch', 'wp-url', 'wp-data', 'wp-i18n', ),
  false,
  array(
    'in_footer' => true,
    'strategy' => 'defer',
    'module_dependencies' => array( '@wordpress/abilities', '@wordpress/core-abilities' ),
  ),
);
wp_enqueue_script( 'example' );
wp_add_inline_script(
  'example',
  <<<JAVASCRIPT
  ( async () => {
    await import( '@wordpress/core-abilities' );
    const m = await import( '@wordpress/abilities' );
    window.m = m;
    m.executeAbility('ai/title-generation');
  } )()
  JAVASCRIPT,
);
Screenshot 2026-05-05 at 10 18 02 AM

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I tested this out and was able to reproduce.

As far as I can tell, it's a race condition. It has more to do with wp-data than with the module system. I didn't see any initial state in the DOM, so the abilities store has an empty ability list until the request to /wp-json/wp-abilities/v1/abilities completes and data is present in the store. Eager evaluation here that expects abilities data to be present is extremely likely to fail because the initial abilities are [].

If I run the following on the browser console, I see the abilities are present (after the page has loaded and data is in the store).

const wpAbilities = await import('@wordpress/abilities');
wpAbilities.getAbilities();
// [ …4 items… ]

I believe that manually enqueuing the module in your case changes the race conditions without eliminating them.

An example like this makes the race clear:

wp_register_script(
	'example',
	false,
	array( 'wp-api-fetch', 'wp-url', 'wp-data', 'wp-i18n' ),
	false,
	array(
		'in_footer' => true,
		'strategy' => 'defer',
		'module_dependencies' => array( '@wordpress/abilities' ),
	),
);
wp_enqueue_script( 'example' );
wp_add_inline_script(
	'example',
	<<<JAVASCRIPT
	( async () => {
		const wpAbilities = await import( '@wordpress/abilities' );
		console.log( 'Initial abilities: %o', wpAbilities.getAbilities().map( ab => ab.name ) );
		const wpData = window.wp.data;
		const selectAbilities = wpData.select( wpAbilities.store )
		const {promise, resolve} = Promise.withResolvers();
		const unsubscribe = wpData.subscribe(
			(...args) => {
				if ( wpAbilities.getAbilities().length ) {
					resolve()
				}
			}
		);
		await promise;
		unsubscribe();
		console.log( 'After subscription: %o', wpAbilities.getAbilities().map( ab => ab.name ) );
	} )()
	JAVASCRIPT,
);

In my testing I see:

Initial abilities: []
After subscription: (4) ['core/get-site-info', 'core/get-environment-info', 'ai/title-generation', 'ai/get-post-details']

Comment thread src/utils/run-ability.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Status] Blocked Used to indicate unable to move forward

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Executing summarization ability with @wordpress/abilities in WordPress 7.0 doesn't work due to invalid schema

3 participants