Skip to content

Conversation

@jorgefilipecosta
Copy link
Member

@jorgefilipecosta jorgefilipecosta commented Dec 26, 2025

Part of: WordPress/ai#40 cc: @Jameswlepage
Equivalent core PR of: WordPress/gutenberg#74234 (in Gutenberg).
Inspired by the work on https://github.com/galatanovidiu/mcp-adapter-implementation-example/tree/experiment/layerd-mcp-tools/includes/Abilities by @galatanovidiu.

Ticket: https://core.trac.wordpress.org/ticket/64455

Core abilities organization

This PR also proposes a logic for how core abilities are organized. In abilities.php, we have two functions: wp_register_core_abilities and wp_register_ability_category (or in the case of Gutenberg, _gutenberg_register_core_abilities and _gutenberg_register_core_ability_categories). These functions then call ability registration functions that are inside the abilities folder. If the ability is simple, it can be registered in just a single internal function, e.g., _wp_register_site_info_ability; for complex abilities, we can register them in a class (like this post management one).

The abilities can be in both Gutenberg and core. Having them in Gutenberg allows us to use them in the workflows functionality that is being worked on by @senadir and allows us to get some testing before core is released.

Gutenberg unregisters the equivalent core ones, so Gutenberg is the source of truth. The same ability can exist in Gutenberg and core, but Gutenberg takes precedence so we can test changes in Gutenberg before releasing in core (similar to what happens with blocks and other WordPress artifacts).

Core Post management abilities

This PR adds core post management abilities for the WordPress abilities API: core/create-post, core/get-post, core/find-posts, and core/update-post.

It supports nested query support for meta_query, tax_query, and date_query matching WordPress's native WP_Query structure with AND/OR relations. This allows very complex query operations which the REST API does not allow and may be useful for agents to find information.

It uses the permission callback mechanism and tries to correctly check permissions for all cases. The basic permission checking logic first checks the basic and common use case (user can edit, create, or see a post), then we go into specifics in separate functions that are reused for checking status changes (e.g., publish), author changes, and taxonomy assignment permissions.

The class WP_Posts_Abilities_Gutenberg is organized into 6 main areas:

  • Ability Registration: The main part that does the wp_register_ability calls.
  • Initialization: Initializes shared data between the abilities, e.g., schemas.
  • Output Formatting: Formats the post object (and taxonomies) for output shared between all abilities.
  • Permission Checking: Permission checking logic; some parts are also shared between abilities.
  • Query Processing: Utilities to process queries and map the ability input format to the WP_Query format.
  • Data Processing: Utilities to process input data, sanitize it, map to other formats, etc.

Missing

The idea of this PR is mainly to get feedback if this is the right direction for the post management abilities. There are some things that are missing:

  • Automated testing: Deeply covering all the abilities. If we agree with this approach, I will add the tests to this PR.
  • Partial Edits: Not all edits are possible, e.g., it is not possible to mark a post as sticky. We need to decide if marking a post as sticky should be done on the update/create post abilities, following the REST API approach, or if we should have a specific ability for that, following the wp_core PHP API approach where we have the wp_update_post function and also stick_post/unstick_post functions. We can decide on what to do regarding the missing edits as follow-ups, as the PR is already too big.
  • Pagination: Pagination was not yet implemented; we need to have a general solution for pagination at the abilities API level.

Test plan

  • Open /wp-admin/edit.php?post_type=post.
  • Open the browser console the following examples.
const abilitiesAPI = (await import( '@wordpress/abilities' ) );
  • Verify core/create-post creates posts with various fields (title, content, status, meta, taxonomies):
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/create-post/run', method: 'POST', data: { input: {post_type: 'post', title: 'Hello World', content: 'My awesome content', 'status': 'publish', meta: {a:23, c:1} } } });
  • Verify core/get-post retrieves posts by ID with optional taxonomy/meta inclusion:
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/get-post/run', method: 'POST', data: { input: {id: 334, include_meta: true, include_taxonomies: true} } })
  • Verify core/find-posts queries work with nested meta_query, tax_query, and date_query.
  • Create posts with the following structure where the title a-23|c-1 represents a post with meta key "a" value of 23 and meta key "c" value of 1. Also adds the correct tags.
Screenshot 2025-12-26 at 17 25 02
  • Execute complex find post queries like:
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: {

    post_type: 'post',

    include_meta: true,

    meta_query: {

        queries: [

            {

                key: 'footnotes',

                compare: 'EXISTS',

            },

        ]

    }

} } });

await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: {

    post_type: 'post',

    include_meta: true,

    meta_query: {

        relation: 'AND',

        queries: [

            {

                key: 'a',

                compare: '=',

                value: '23'

            },

            {

                relation: 'OR',

                queries: [

            {

                key: 'b',

                compare: '=',

                value: '1'

            }, {

                key: 'c',

                compare: '=',

                value: '1'

            } ] }



        ]

    }

} } });

await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: {

    post_type: 'post',

    include_meta: true,

    tax_query: {

        relation: 'AND',

        queries: [

            {

                taxonomy: 'post_tag',

                field: 'slug',

                terms: ['t-23']

            },

            {

                relation: 'OR',

                queries: [

                    {

                        taxonomy: 'post_tag',

                        field: 'slug',

                        terms: ['c-1']

                    },

                    {

                        taxonomy: 'post_tag',

                        field: 'slug',

                        terms: ['b-1']

                    }

                ]

            }

        ]

    }

} } });



await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/find-posts/run', method: 'POST', data: { input: {

    post_type: 'post',

    date_query: {

        relation: 'AND',

        queries: [

            { year: 2025 },

            {

                relation: 'OR',

                queries: [

                    { day: 26 },

                    { month: 11 }

                ]

            }

        ]

    }

} } });
  • Verify core/update-post modifies existing posts correctly:
await wp.apiFetch({path: '/wp-abilities/v1/abilities/core/update-post/run', method: 'POST', data: { input: {
    id: 327,
    content: 'Hello'
} } });
  • Test permission checks prevent unauthorized access, e.g., users without publish permissions trying to change status to published, etc.

@jorgefilipecosta jorgefilipecosta added enhancement php Pull requests that update php code labels Dec 26, 2025
@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@github-actions
Copy link

github-actions bot commented Dec 26, 2025

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.

Core Committers: Use this line as a base for the props when committing in SVN:

Props jorgefilipecosta, jason_the_adams.

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

@jorgefilipecosta jorgefilipecosta force-pushed the add/post-management-abilities branch from af66fd3 to e9aa703 Compare January 14, 2026 13:03
@JasonTheAdams
Copy link
Member

I'm not entirely confident in my thought here, but it is one I've wrestled with so I'll share it here open-handedly. 😄

I go back and forth on the idea of polymorphic abilities like this. That is, should the create-post ability be able to create any post? Or strictly the post post type? In which case there'd be other abilities such as create-page, create-attachment, and so forth.

On the one hand, this is consistent with how the underlying functions work. But do we simply want to surface low-level functions like that to abilities? Are Abilities low level in the same way and are merely another form of function? I'm inclined to say no, my reason being that Abilities are designed to be multi-contextual — direct execution, REST, Command Palette, MCP, AI function declarations, etc. As input and output parameters get more complex, adapting them to these various contexts gets harder. Some contexts handles certain complexity well, while others get tricky fast.

Having more specific Abilities also means we can take things like pinning, page templates, attachment sizes, and so forth, and include them as part of the input/output parameters. If we go generic, then we're back to addressing things as meta, which is not as discoverable, not as easy to work with, more fragile, and so forth.

Now, one could then wonder about how Abilities work with custom post types, especially those that work very similarly to posts. If there are specific Abilities, then how do those get their own Abilities without folks having to always put in effort to make them? I propose updating register_post_type to have an $args['register_ability'] parameter which can be:

  • true
  • false
  • array( 'create' => true/false, 'read' => true/false, 'update' => true/false, 'delete' => true/false

This would use the slug to optionally create create-$slug, get-$slug and so forth Abilities. If the custom post type is more complicated and wants to add it's own Abilities, it can opt-out and do so with no problem.

Curious what others think! The more I see Abilities used, especially multi-contextually, the more convinced I am that simpler and more focused is better than complexities such as polymorphism.

@jorgefilipecosta
Copy link
Member Author

Thank you a lot for sharing you thoughts @JasonTheAdams.
I'm also not certain if we should have a single set of abilities to manage all post types or abilities per post type, with a mechanism to automatically generate them.

My idea was that both approaches would be complementary we would have a raw building block that supports managing any kind of post. And then specific post types needing more flexibility could register their own abilities which internally may even call or reuse parts of the core one.

I think we may also give it a try to the abilities per post type approach (with functionality to automatically generate abilities for a post type) so we can compare both live. The downside I see with that is that we have more abilities, e.g: in a site with 10 simples CPT's using the default management abilities we may easily have 10*4 =40 post management abilities vs just 4 generic ones, which may pollute a little bit the context on LLM agents but if we have good search and filtering abilities that may be acceptable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement php Pull requests that update php code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants