[wp-trac] [WordPress Trac] #64345: Ability names should support versioning
WordPress Trac
noreply at wordpress.org
Sat Jan 17 17:32:59 UTC 2026
#64345: Ability names should support versioning
--------------------------------------+------------------------------
Reporter: jason_the_adams | Owner: (none)
Type: enhancement | Status: new
Priority: normal | Milestone: Awaiting Review
Component: AI | Version: trunk
Severity: normal | Resolution:
Keywords: has-patch has-unit-tests | Focuses:
--------------------------------------+------------------------------
Comment (by justlevine):
@JasonTheAdams - illustrative examples are always helpful 🙇
I want to highlight that the "two sides" you're describing aren't
deprecation and ''versioning'', but rather deprecation and ''naming
things'' - which as one [https://www.namingthings.co/ of the hardest parts
about development] seems pretty beyond the scope of what we can solve with
our API and so IMO shouldn't be given as a reason to compromise on DX.
(I think you get to this in the end when you noted for you it mostly boils
down to "subdomains", but I wanted to be explicit and upfront since it's
an important distinction).
Now for {{{givewp/create-donation}}}:
> First, I'll need to create a new ability so the old one still works to
at least some extent, or for a certain period of time.
This feels like a false premise. Instead of a new ability, you ''could''
mark the new {{{campaign_id}}} as schematically optional, but note in the
description and deprecation message for an empty value that it will be
required in the future.
(As a reminder, versionless schemas are a long-existing engineering
pattern. It's not about the specific example, the majority of examples
have a versionless path of evolution).
However, let's assume that for some reason you '''choose''' to break
things with the old ability and create a new one.
> This presents the new dilemma: what do I name the ability? givewp
/create-donation-with-campaign? givewp/cteate-donation-final? It's still
the same thing, it just has different input and output parameters.
This is the generic "naming things" problem. It's the same problem when
you want to deprecate a public function, a WordPress hook, etc. Versioning
does not solve this problem any differently than other forms of prefixing.
You communicate the changes the same way you would with any other rename
to a "new" thing. So using versioning for this purpose is all of the
baggage without IMO any of the benefits.
The difference with a ''versionless'' approach is that by design it's easy
and encouraged to ''not'' create a new ability but rather evolve the
existing one, so you avoid needing to make pedantic differentiations in
the first place.
> Now, one could argue that in this case GiveWP should somehow make the
Ability backwards compatibility. I'm not sure how that would be possible,
but in any case it forces architectural philosophy on the developer. We
don't know them or their market, so I want to avoid that.
I don't think I followed this. Moving beyond the fact that Abilities are
literally meant to bring WordPress-levels of backwards compatibility to
3rd party APIs, how does prioritizing backwards compatibility "force
architectural philosophy" on the developer? If a dev is gung ho on a
versioning antipattern, they can just as easily choose not to back compat
and release a new ability as described. Nothing is changed if we allow any
number of %s/ segments versus only a singular v%n/.
> Having Abilities that grow in complexity like that hurts discoverability
and reduces the chances of it working easily in many contexts.
It's because I agree with this that I'm strongly advocating for a
versionless approach. Complexity isn't measured in terms of args (beyond
general API best practices — if you've deprecated a dozen args, you might
want to cut a new ability), it's measured in terms of how many similar but
slightly different abilities you (or your context window) need to juggle.
Versionless by default means that changes rarely invalidate docs, best
practices, tutorials, but are '''additive''' to the existing body of
knowledge.
> As a note, "subdomains" in the Ability name is effectively what's being
proposed here, at least in its simplest form. I'm leery about trying to
shove in too much of a versioning concept, which is why I'm presenting a
"versioned" Ability as actually multiple Abilities.
Agree here (as noted up top). Aligning abilities to block naming and
limiting them to {plugin_name}/{block_name} was an arbitrary v1 limitation
in part because some folks were trying to carve out a use case for Ability
Categories. But letting abilities have subdomains — especially and
ironically since categorization is now a distinct concept — lets folks who
want to use a versioning antipattern (and many other possible use cases)
do so without forcing a specific design pattern on everyone.
IMO we should detwine the topics, and move the versioning discussion out
of both the namespace and this ticket, and refocus on generally allowing
----
@jorgefilipecosta - thanks for this suggestion and making the parallel to
blocks.
In addition to addressing a similar need as us, IIRC (so please correct
me) the specific API shape there evolved as a result of release pressures
and API limitations with the original implementations, as headaches around
block deprecations were one of the major pain points developers, agencies,
and enterprises cited for years as reasons against migrating to blocks.
So while it's matured very well — and might be worth doing here even just
for the sake of consistency — I'd want to take a beat and consider what a
holistic approach would look like for Abilities, and their differing
needs. (Still very much rather this approach than the versioning ideas
being thrown around).
My gut concern with a separate {{{deprecated}}} array of snapshots (? or
would we do this composably so you only include the diff in the lower
levels?) would boil down to discoverability. Unlike with GB APIs where we
just want to map and transform data, the input/output schemas are meant
for both human and agentic inference. Would these deprecations map to a
one-of (and if so how do we indicate the priority), or are we clobbering
them all down to a single schema (which ties back to @jasontheadams's
concern about complexity/schema creep and sabotaging our context windows)?
How do we indicate (to humans or traditional/inference machines) what the
migration path is from deprecated value to current? etc.
We can probably come up with answers to these, or more holistic
alternatives might be blocked by 6.9 tech debt, and I hope we explore it
further.
---
@artpi as previously noted (and illustrated by the woo example) versioning
is a mechanism for communicating behavior — in and of itself it does not
guarantee backwards compatibility or predictable behavior, though it can
be coopted to do so.
Using that to add some (opinionated) nuance to your helpful breakdown of
concerns:
> 3: When depending on an ability
> Let's say plugin B is using an ability provided by plugin A:
> You very often want to be able to pin an ability version that behaves in
a predictable way.
The hidden premise here is "pin an ability version". I believe it should
be rephrased as "You very often want to ensure an ability behaves in a
predictable way."
Versionless schemas — and many other API patterns for backcompat (e.g.
most parts of WordPress core) — solve this ''without'' needing to pin
versions.
> Lets imagine abilities used for core financial operations: Handling
floats in financial operations is something we had to learn the hard way
multiple times in WooCommerce:
Perfect example, thanks. I want to highlight that headaches around
WooCommerce's breaking ecosystem changes since ditching SemVer was a huge
motivator and specific design influence for almost everything about the
Abilities API.
The crux of the problem was that '''There was no schema changes, and yet
everything changed'''. If everything changed, then '''Woo should have
"changed the schema"''' or provided a method for backcompat. Neither
versioning nor the Abilities API can full stop a developer for shipping a
breaking change in behavior (that's the purpose of unit tests), but what
the Abilities API does — regardless of versioning — is ensure that the
only way you can break the API Contract (i.e. consumer ''expectation'') is
with an '''intentional''' schema change.
Here's btw what that could have looked traditionally (PHP pseudocode)
without any Abilities schema enforcement:
{{{#!php
<?php
// conditional arg to support legacy behavior.
handle_financial_operation( ...$existing_args, $legacy_float_mode: bool =
false ) {
if ( $legacy_float_mode === false && Woo::initial_install_date(
$before_this_change ) ) {
_deprecated_arg( 'We fixed floats, fix before the breaking release
when we change the default behavior.' );
...
}
// new behavior
}
// or we deprecate the entire method.
handle_financial_operation( ...$existing_args ) {
// if you're really feeling the PM pressure to version.
_deprecated_method( 'Use Woo\V2::handle_financial_operation()
instead');
// Same as before we can choose to support the old behavior or after a
time coerce to new behavior etc.
}
// or we could just create separate functions.
calculate_financial_operation( ...$new_args ) {
// new behavior
}
}}}
(I hope it's clear how that can be translated conceptually to Abilities,
either with @jorgefilipecosta's approach or mine.)
> It really sounds like a good fit for an additional version field:
> ...
> wp_get_ability( 'artpi/sum', 1 );
This is the anti-pattern I'm trying to avoid here.
(Although as mentioned higher in the thread, at least it's relegated back
to meta and no longer poisoning namespace semantics 😅).
With this change, instead of versions being an optional and voluntary way
to handle the "naming things" issue it's now a hard-coded, public API
'''requirement'''. Every consumer needs to be on the lookout and able to
handle versioning on every single ability — people and agents wasting
cognitive load (and context window) checking potential breaking v2 whose
existence is permanently inferred from the existence of {{{version}}}.
----
From replies above it seems some folks think I'm proposing a philosophical
idea and not a design pattern, so to help visualize my suggestion in terms
of code instead of concepts, I'm proposing (pseudocode):
{{{#!php
<?php
wp_register_ability(
'plugin/my-ability',
array(
'label' => __( 'My ability' ),
'description' => __( 'My ability.' ),
'input_schema' => array(
...$unchanged_fields,
'my_old_input' => array(
'type' => 'string',
'description' => __( 'Old input field, deprecated.' ),
// One possibility: a minimally viable ?string with a
semantic message.
'deprecation_reason' => __( 'Use "my_new_input" instead.
This field will be removed in a future release.' ),
),
'my_new_input' => array(
...$field_definition
)
),
'output_schema' => array(
...$same_concept_as_input_fields
),
'execute_callback' => static function ( $input ) {
// Developers can do WHATEVER they want here to route to
internal behavior or drive results,
// yet NONE of it is a breaking change, or anything a
downstream consumer needs to worry about.
// If developers _choose_ to break behavior, use versions
(from meta) to route deprecated behavior,
// or use flags-as-args, it's entirely immaterial to API
}
)
);
}}}
Obviously we can make this more sophisticated (e.g. a {{{deprecation}}}
array with a {{{since}}}, {{{reason}}}, {{{replacement}}}) for more
agentic consumption — if justified — I'm just trying to illustrate how
simple it is to go the versionless route as best practice while still
allowing for developers to build whatever versioning system they want.
@JasonTheAdams - from a discovery and DX perspective, the way GraphQL
handles deprecated fields is via progressive disclosure (another way
versionless aligns with agentic requirements). Deprecated fields are only
visible for introspection, so they'll still work internally, but only show
up in tooling or agentic requests if the entire schema with deprecations
is requested.
Programmatically, the implementation question for doing the above is how
to handle our existing reliance on {{{rest_sanitize_value_for_schema()}}}
for validation. While even modern JSON Schema supports at minimum a
''deprecated'' field, we're using WP_Rest-flavored JSON. We could do a
pre-loop to map/strip our dx to ''additionalProperties'' or similar
engineering solutions to evaluate.
------
'''tl;dr''' I'm hoping for consensus on the following decision:
> Anything versioning offers can be implemented gratis by developers if we
take a versionless approach to deprecations.
>
> However, adopting versioning forces ''all'' developers to version, and
the shortcomings with that are numerous and well documented
> (even if you personally disagree with how severe they are or whether the
pros outweigh the cons of various approaches some cases).
If/once agreed, we can detangle the two conversations (tickets?) into:
1. a holistic approach to (versionless) deprecations
2. Opening up ability to namespace subdomains ''of any shape''. (That it
supports folks who want to implement versioning via the name, and not via
meta - if at all - is just a "happy" side effect.)
This would let us more effectively handle both in time for 7.0b1
--
Ticket URL: <https://core.trac.wordpress.org/ticket/64345#comment:12>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list