/resources/abilities-api/designing-ability-schemas/
What We Do
Digital Platform MigrationsKey SolutionsManaged ServicesArtificial IntelligenceIndustriesProducts
OnePress
Unify multiple brands on one governed WordPress platform.
Design & UI/UX
Gutenberg-native UX, UI, and design systems for visitors and editors.
WordPress Modernization
Modernize WordPress for better performance, architecture, and AI readiness.
WordPress as a DXP
WordPress as a composable DXP when monolithic systems no longer cut it.
Headless WordPress
Omnichannel content delivery without sacrificing marketing autonomy.
Frappe/ERPNext
Build scalable ERP and custom applications, from implementation to ongoing support.
Discovery
Strategic consultancy & project roadmap
Growth Services
On demand development & consultation
Site Maintenance
Annual maintenance. Done for you
QE Services
Testing across SDLC for assured quality
Hosting Migration
Move to a performant hosting with zero downtime
WooCommerce
Enterprise commerce delivered without lock-in
Staff Augmentation
Expert WordPress engineers, onboarded in a week
All Services
A suite of services for any need

Publishio : Publish on your WordPress website from Claude & ChatGPT

Extending the WordPress AI Plugin Releasing Connectors for OpenRouter, LM Studio & Universal OpenAI

How rtCamp saves 100 hours of teams using AI powered Sales Qualification
eCommerce
Scale your e-commerce with WooCommerce, integrations, and custom extensions for growth.
EasyEngine
Server management tool that makes using WordPress on Nginx easy.
Web Auditor
Performance Audit & Insights for your Website.
rtMedia
A complete media management plugin for WordPress.
Resources

About Us

CLEAR
Resources
A practical guide to the WordPress Abilities API
Part 3: Designing Ability Schemas
Topics
On this page
- A practical guide to the WordPress Abilities API
- Part 1: Thinking in Abilities
- Part 2: Building with Abilities
- Part 3: Designing Ability Schemas
The Importance of API Design
Principles of good schema design
1. Start from what the caller wants done, not from your implementation
2. Say exactly what you mean
3. Focused beats flexible
4. Leave room to grow
5. Design for immediate consumption
Where we are
Thin inputs, rich outputs
Input Schemas
Output Schemas
Modeling Errors and Edge Cases
1. Model every expected outcome
2. Be consistent with WP_Error
3. Make "nothing" a valid response
4. Handle partial success with an errors object
Evolving your schema (without breaking existing consumers)
Deprecating Abilities
Handling Ability Deprecation
Next Time: Scaling with Abilities
Last updated on Oct 8, 2026
Designing Ability Schemas
Welcome to the third part of our series on developing with WordPress’s Abilities API. In Part 1: Thinking in Abilities, we introduced Abilities as functional primitives that allow you to rely on external code without worrying about internal implementation details. In Part 2: Building with Abilities, we looked at how Abilities fit alongside existing WordPress coding patterns and the different considerations and ways to implement them effectively.
This time, we’re diving deep into the most important aspect of Abilities: their input and output schemas. We’ll cover:
- What makes a good input and output schema, and how to design for future-compatibility and immediate consumption
- How to model errors, empty results and other edge cases so callers don’t have to guess
- How to evolve a schema without breaking backward-compatibility
- Why versioning your Abilities is an anti-pattern, and what to do instead
Let’s get started!
The Importance of API Design
As discussed earlier, Abilities work by enforcing a strict schema contract on both their inputs and outputs, and that contract is what lets us build on top of them without worrying about how they’re implemented. If I’m expecting user or post data, I can trust that’s what I’ll actually get, no matter what changes inside the Ability.
If this sounds similar to strict type checking, you’re not wrong, and if the WordPress ecosystem followed SemVer, then maybe we never would have needed to create the Abilities API to begin with. Abilities go beyond that. They are an explicit promise from their author that this is meant to be publicly consumed, and that this is the shape it provides.
To keep that promise, it’s useful to think about not just whether something should be an Ability, but also about how it will be consumed. That’s not to say we need to get the Ability’s shape perfect on the first try. A well-designed Ability schema can be iterated on without breaking the contract or the integrations and training data that rely on it.
Say we’re building e-commerce functionality, and orders need to be placed from multiple channels: a traditional frontend checkout flow, an admin screen the support team uses for phone orders, and a wp-cli command for automated order processing. With multiple consumers sharing the same piece of business logic, we have a good candidate for a new Ability, so we DRY our code out:
wp_register_ability(
'rt-commerce/create-order',
[
'label' => __( 'Create Order', 'rt-commerce' ),
'description' => __( 'Create a new order.', 'rt-commerce' ),
'category' => 'ecommerce',
'input_schema' => [
'type' => 'object',
'properties' => [
'user_id' => [ 'type' => 'integer' ],
'products' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
'quantities' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
'coupon' => [ 'type' => 'string' ],
'billing_first_name' => [ 'type' => 'string' ],
'billing_last_name' => [ 'type' => 'string' ],
'billing_email' => [ 'type' => 'string' ],
// ...and the rest of the billing fields.
'shipping_same_as_billing' => [ 'type' => 'boolean' ],
// ...and every shipping_* field to match.
'payment_method' => [ 'type' => 'string' ],
'stripe_token' => [ 'type' => 'string' ],
],
'required' => [ 'user_id', 'products', 'payment_method' ],
],
'output_schema' => [
'type' => [ 'integer', 'boolean' ],
'description' => __( 'The order ID, or false on failure.', 'rt-commerce' ),
],
'permission_callback' => static fn ( array $input ) => current_user_can( 'order_products', $input['user_id'] ?? 0 ),
'execute_callback' => static function ( array $input ) {
$order_obj = new Rt_Commerce\Order();
$order_obj->set_customer( $input['user_id'] );
foreach ( $input['products'] as $i => $product_id ) {
$order_obj->add_product( $product_id, $input['quantities'][ $i ] ?? 1 );
}
if ( ! empty( $input['coupon'] ) ) {
$order_obj->apply_coupon( $input['coupon'] );
}
// ... and so on, until the order object is populated and ready for submission.
if ( ! Rt_Commerce\Payments\Stripe::charge( $order_obj, $input['stripe_token'] ) ) {
$order_obj->update_status( 'failed' );
return false;
}
// If we're here the order succeeded
$order_obj->update_status( 'completed' );
return $order_obj->get_id();
}
]
);
// Userland code:
$ability = wp_get_ability( 'rt-commerce/create-order' );
$result = $ability->execute( [
'user_id' => get_current_user_id(),
...$other_order_data,
] );
The Ability is pretty standard. It takes the user ID, product, billing and payment information, and attempts to place an order. It returns the order ID if the order is successfully placed, or false on failure. Now, let’s say we want to add support for guest checkout:
'input_schema' => [
'type' => 'object',
'properties' => [
- 'user_id' => [ 'type' => 'integer' ],
+ 'user_id' => [ 'type' => [ 'integer', 'null' ] ],
// ...
- 'required' => [ 'user_id', 'products', 'payment_method' ],
+ 'required' => [ 'products', 'payment_method' ],
// ...
'execute_callback' => static function ( array $input ) {
$order_obj = new Rt_Commerce\Order();
- $order_obj->set_customer( $input['user_id'] );
+ $order_obj->set_customer( $input['user_id'] ?? null );
// ... rest of the execute_callback function
The previous downstream usage of ability->execute( ['user_id' => get_current_user_id(), ...] ) will continue to work, and you don’t need to change anything. Heck, some new developer or an AI-slopbot could refactor the whole Order API, and your usage would still remain the same.
Now, let’s fast-forward:
- Marketing wants coupon stacking.
couponis a string, and turning it into an array would break the existing schema. So you add a newcouponsfield next to it and now there are two ways to apply a discount, plus validation rules to handle the submission of both. - Client switches payment providers. The
stripe_tokenis part of the contract, deeply embedded in the storefront’s JavaScript, and in all the docs and AI training data. - Some payments need customer action. 3-D Secure starts sending customers to their bank to confirm payment, but downstream we’re only expecting an order ID or
false. - We start selling digital subscriptions. Now orders can have recurring billing, don’t need a shipping address or quantities, and humans and AI shopping assistants are parsing through an entire list of fields trying to figure out what applies to each type of order.
- The storefront adds a confirmation page. Now for every order, you’re making another request to fetch and validate the order details by ID. On a high-traffic ecommerce site, those extra database queries quickly add up.
Each of these is a pretty common feature request, but fixing them now requires either introducing a breaking change or compromising on the usability of the schema. So you create a rt-commerce/create-order-v2, and leave the original in place for backwards compatibility. Next time, we add a `-v3`, then a `-v4`, and now we’re maintaining multiple versions of the same Ability, and preempted the entire reason we chose to isolate this functionality.
A strict schema isn’t automatically a good schema. We want a schema that is both resilient to slop and flexible enough to adapt to changes so that our APIs can evolve with us. Otherwise, we might as well have just used a traditional PHP API.
Principles of good schema design
The good news is that even though the Abilities API is new, we don’t need to invent holistic API design best practices from scratch. The Abilities API was heavily influenced by (WP)GraphQL, and the GraphQL community has spent a decade working out what makes a schema both pleasant to consume and safe to evolve. Most of that thinking carries straight over.
Let’s go through some of the more important principles, and see how they apply to our create-order Ability.
1. Start from what the caller wants done, not from your implementation
Look back at our original input schema, and you’ll notice it’s a list of everything the execute_callback needs, in the order it needs it. user_id exists to set_customer(), the billing_* fields exist because our Order class has setters for each of them, and stripe_token exists because – at the time of writing – Stripe was the only payment provider we supported.
This is called inside-out design: we started from the implementation details (inside) and exposed whatever it needed to our consumers (outside). Of course our schema became fragile and broke as soon as the implementation details changed, since it was just a description of our Order class and not a contract for placing orders.
The fix is to design from the outside in. Before writing any schema, list the jobs your consumers are trying to get done:
- The storefront wants to turn a cart into a processed order.
- The support team wants to place orders on behalf of a customer.
- The CLI wants to bulk import orders from another system.
None of those jobs mention Stripe, while all of them involve paying for something. So the contract should describe payment, and leave which provider to the implementation:
- 'payment_method' => [ 'type' => 'string' ],
- 'stripe_token' => [ 'type' => 'string' ],
+ 'payment' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'method' => [
+ 'type' => 'string',
+ 'enum' => [ 'card', 'bank_transfer', 'cash_on_delivery' ],
+ ],
+ 'token' => [
+ 'type' => 'string',
+ 'description' => __( 'An opaque payment token issued by the store\'s payment provider. Required when `method` is `card`.', 'rt-commerce' ),
+ ],
+ 'billing_address' => Rt_Commerce\Schema::address(),
+ ],
+ 'required' => [ 'method' ],
+ ],
Now, when the client moves from Stripe to Square, the storefront tokenization script needs to change, but the Ability’s contract stays the same (and with it the admin screen and CLI command).
A good test for any field: can you switch out the thing behind it without changing the schema? If the answer is no, you’ve probably coupled your schema too tightly to the implementation details.
2. Say exactly what you mean
It’s not just human developers that read your schema. So do REST clients, headless frontends, and other programmatic integrations, and, with MCP and whatever agentic protocols emerge, LLMs read it to decide both whether to use it and what to pass when they do. Every name and description in your schema needs to serve as both documentation and AI context.
Start with the ability’s name. create-order sounds clear at first glance: it creates a record. But our ability also charges a credit card. An agent asked to “create a draft order for the customer to review” has every reason to reach for create-order… until the customer gets billed. A name should describe the outcome, including any side effects. In commerce parlance, “create an order and take the money” is place. rt-commerce/place-order tells you what’s happening before you’ve read anything else.
(Yes, renaming an Ability is a breaking change. We’ll deal with that properly later; for now, assume we caught it in code review.)
The same goes for fields. products is an array of item IDs, so the name should say so. coupon could be a code, an ID to a Rt_Commerce\Coupon, or an object. Use specific names, and use them consistently – if it’s an item in place-order, then it shouldn’t be a line in your get-order Ability or a product in refund-order.
3. Focused beats flexible
It may sound counterintuitive, but the best way to keep a schema flexible over time is to keep each part of it inflexible. Every optional field that offers another way to do the same thing is now something you have to support, test, and document forever. There’s a user_id, which can now be null for guests. There’s a billing_email, which is essential for guests but optional for registered users (we know their email). So what happens when a caller sends a user_id and a billing_email? The schema has no opinion, which leaves it up to whatever the implementation is doing this month.
When a field can mean different things in different contexts, it’s best to make each situation explicit:
'customer' => [
'description' => __( 'The customer placing the order: either a registered user, or a guest identified by email.', 'rt-commerce' ),
'oneOf' => [
[
'title' => 'Registered customer',
'type' => 'object',
'properties' => [
'user_id' => [ 'type' => 'integer', 'minimum' => 1 ],
],
'required' => [ 'user_id' ],
'additionalProperties' => false,
],
[
'title' => 'Guest',
'type' => 'object',
'properties' => [
'email' => [ 'type' => 'string', 'format' => 'email' ],
'name' => [ 'type' => 'string' ],
],
'required' => [ 'email' ],
'additionalProperties' => false,
],
],
],
A caller now picks either a registered customer user_id or a guest, and we’re able to evolve the requirements of each independently.
We can apply the same thinking to boolean flags: shipping_same_as_billing is a true/false indicator that can’t scale to handle in-store pickups or digital products that don’t ship at all, or. An enum or a oneOf union can gain new values without breaking the schema:
'fulfillment' => [
'oneOf' => [
[
'title' => 'Ship to address',
'type' => 'object',
'properties' => [
'method' => [
'type' => 'string',
'enum' => [ 'shipping' ],
],
'address' => Rt_Commerce\Schema::address(),
],
'required' => [ 'method', 'address' ],
],
[
'title' => 'No fulfillment needed (digital goods)',
'type' => 'object',
'properties' => [
'method' => [
'type' => 'string',
'enum' => [ 'none' ],
],
],
'required' => [ 'method' ],
],
],
],
Note: WordPress-flavored JSON Schema doesn’t support const, and unsupported keywords are silently ignored rather than rejected, so 'const' => 'shipping' would be ignored. A single-value enum accomplishes the same thing.
Being focused also tells you when not to share. Supporting subscriptions doesn’t just require additional fields, it’s a different outcome from a one-time order that requires a billing interval, renewal date, and cancellation policy. Maybe there’s room in place-order for both. Or, instead of giving the caller a schema where half of the fields don’t apply, you register a separate rt-commerce/start-subscription Ability – it can even call place-order internally.
4. Leave room to grow
Most breaking changes happen when a value needs to support more than it used to. A string becomes a list, a number needs a unit, a flag has to accommodate a third option. You can’t predict every future requirement, but you can keep the cost of growth low, and the cheapest way to do that is by nesting your fields in dedicated objects. In fact, we’ve already done this in some of the above examples.
Take the original products and quantities: two parallel arrays that need to keep their indexes aligned. A caller can send three products and two quantities without the schema noticing, and as soon as we need product variations or product-specific metadata, there’s nowhere to put it.
- 'products' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
- 'quantities' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ] ],
+ 'items' => [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'product_id' => [ 'type' => 'integer' ],
+ 'quantity' => [ 'type' => 'integer', 'minimum' => 1 ],
+ ],
+ 'required' => [ 'product_id', 'quantity' ],
+ ],
+ ],
Each item is now a self-contained object, which doesn’t just make the schema easier to reason about but also means we can add optional fields like a variation_id or a personalization_note without breaking existing clients. Same goes for the flat billing_* and shipping_* fields. Grouping them into an address object (our fictitious Schema::address() above), means we can reuse the existing shapes for billing, shipping, pickup locations, and anything else that turns out to need one.
Let’s take coupon as another example. coupon was a single string because at the time you could only use one coupon per order. But just knowing that multiple coupons are plausible makes it smart to model it as a list from the get-go:
'coupons' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'code' => [ 'type' => 'string' ],
],
'required' => [ 'code' ],
],
],
Why coupons.code instead of just a flat list of strings? Because one day when we support gift cards or loyalty points, we might need a type or an amount. Turning a string into an object is a breaking change; adding a field to an object isn’t.
Importantly, there’s a difference between nesting (what we’re doing here) and adding speculative fields. Wrapping code in an object costs nothing in terms of your schema contract, as it’s not promising anything new to your clients. Adding a gift_card_pin field “just in case” means you need to support it forever, whether or not anyone ever uses it. Nest freely, but wait to add fields until they’re actually needed.
This applies doubly to output shapes: a top-level integer|false is the least extensible thing you can return. Always return an object, even if it only has one property.
5. Design for immediate consumption
Remember how the confirmation page requires making a second request for every order? That’s because our output only answered “did it work?” instead of a more holistic question like “what happened, and what do I do next?”
A good output gives the caller everything it needs to take the next step without coming back for more requests. For an Ability that changes something, that usually means returning the thing it changed:
'output_schema' => [
'type' => 'object',
'properties' => [
'order' => Rt_Commerce\Schema::order(),
],
'required' => [ 'order' ],
'additionalProperties' => false,
],
/**
* Rt_Commerce\Schema::order() returns, roughly:
* [
* 'id' => integer,
* 'status' => enum: pending_payment | processing | completed | failed,
* 'items' => [ { product_id, quantity, unit_price, subtotal } ],
* 'totals' => { subtotal, discount, shipping, tax, total, currency },
* 'payment' => {
* 'status' => enum: paid | requires_action | failed,
* 'next_action' => null | { type: 'redirect', url },
* },
* 'receipt_url' => string,
* ]
*/
Now the storefront can render the confirmation page straight from the response. It can send the customer to their bank when 3-D Secure kicks in, or provide helpful follow-ups to multi-step agentic tools.
$result = wp_get_ability( 'rt-commerce/place-order' )->execute( $order_input );
if ( is_wp_error( $result ) ) {
return $result;
}
$order = $result['order'];
if ( 'requires_action' === $order['payment']['status'] ) {
wp_redirect( $order['payment']['next_action']['url'] );
exit;
}
// No second query. Everything the confirmation page needs is already here.
render_order_confirmation( $order );
// And the output composes: the ID slots straight into the next Ability.
wp_get_ability( 'rt-commerce/send-order-confirmation' )->execute( [ 'order_id' => $order['id'] ] );
We’ll come back to how to model outcomes like this later on, but notice how the example is using Schema::order() to allow for the same output shape to be reused for get-order, list-orders, and anywhere else that needs to represent an order. A consumer who learns what an order looks like can handle it everywhere, and doesn’t need to care which Ability it came from.
Where we are
Here’s what calling our redesigned rt-commerce/place-order Ability looks like so far:
wp_get_ability( 'rt-commerce/place-order' )->execute( [
'customer' => [ 'email' => '[email protected]', 'name' => 'Jane Doe' ],
'items' => [
[ 'product_id' => 42, 'quantity' => 2 ],
[ 'product_id' => 77, 'quantity' => 1 ],
],
'coupons' => [ [ 'code' => 'SUMMER10' ] ],
'fulfillment' => [ 'method' => 'shipping', 'address' => [ /* ... */ ] ],
'payment' => [ 'method' => 'card', 'token' => 'tok_abc123', 'billing_address' => [ 'name' => 'Jane Doe', 'line_1' => '1 Main St', 'city' => 'Springfield', 'country' => 'US' ], ],
] );
Every field on our ability has clear scope and can accommodate all of our initial hypothetical evolutions without breaking the schema or leaking implementation details.
Thin inputs, rich outputs
Input and output schemas protect different aspects of an integration. The input schema protects our implementation from the caller, so the Ability doesn’t need to know or care about the nuances or specificities of the userland lifecycle that it’s called from. Output schemas, however, guard the integration’s outcome and protect the caller, and therefore must be proactive to serve the UI, CLI output, agentic call-chain, or whatever the consumer is trying to accomplish by using the ability.
As such, the overarching rule for Ability design is to keep your Inputs thin, requiring the fewest details needed to execute the task, while keeping the Outputs rich with extra data, providing whatever can be cheaply returned so the caller doesn’t have to come back and ask.
Input Schemas
1. Only ask for what you can’t infer or derive
When your checkout flow calls place-order, it probably already knows a lot about the customer, cart items, shipping details, and more. In traditional API design – especially in modern codebases that favor explicitness and DI – it usually makes sense to pass along that information explicitly, rather than require the implementation to reconstruct it. However, Abilities work best when they’re devoid of that additional context, allowing them to be used in a much broader range of scenarios.
You can see above in our use of ids for our user, product, and coupons (the code is a unique identifier). By only requiring the reference, instead of passing the array-equivalent of those objects, the caller no longer needs to build those objects just to consume the Ability. You may have that built object in a traditional frontend, but your CSV import probably doesn’t, and forcing it to construct one defeats the purpose.
As a rule: if you can look it up or calculate it, it’s not an input. Instead, the caller should just tell us what they want (e.g. the specific product_id and quantity), and we can work out the price, tax, applied discounts, etc., internally, where we can evolve our logic without requiring the consumer to evolve theirs as well.
2. Fewer fields mean fewer ways to break the contract
Every optional field we add multiplies the number of combinations we need to support. As mentioned above, nesting is one way to address this – the fewer same-level fields we have, the fewer combinations we need to consider, and the easier it is to maintain a stable contract.
If a field only makes sense for one caller, that’s usually a sign that the field doesn’t belong in a shared input schema, and the first step should be to ask whether the caller is looking for a different outcome than the one our ability is designed to support. Only if not should we consider adding it, ideally in a nested path that limits its impact on other callers.
For example, when storefront visitors make a purchase, the site needs to send an email confirmation or a stock update, but we don’t want to be spamming our end users when doing a CSV import over CLI, while support staff in the dashboard may or may not want to dispatch a receipt email, depending on what action they’re performing. While it may be tempting to add a send_email_confirmation flag to the schema, it’s likely better to create a separate rt-commerce/send-order-confirmation ability that takes an order ID and can be invoked independently of the main place-order flow. Or it might make sense to decouple the wp-cli use case from our current Ability and create a separate rt-commerce/import-orders Ability that’s wholly devoid of email-related concerns.
When in doubt, fall back to nesting. An email_options object, for example, can encapsulate all email-related settings and flags, without conflicting with the other top-level schema fields.
3. Start strict, loosen later
The thinner the input, the easier it is to keep it strict. Similarly, bounded input limits allow us to keep control and prevent unexpected values from creeping in or even outright abuse.
The first step for enforcing strictness is to make sure you set 'additionalProperties' => false on every object in your input schema, including the nested ones like items. This provides immediate feedback to consumers when they include unexpected fields, like a hallucinated gift_message field from an over-contexted MCP assistant. For individual fields, we want to use enum, pattern, minLength, minItems, and similar constraints to enforce our expectations for what the implementation details should assumedly handle:
'items' => [
'type' => 'array',
'minItems' => 1,
'maxItems' => 100,
'items' => [
'type' => 'object',
'properties' => [
'product_id' => [ 'type' => 'integer', 'minimum' => 1 ],
'quantity' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 999 ],
],
'required' => [ 'product_id', 'quantity' ],
'additionalProperties' => false,
],
],
'coupons' => [
'type' => 'array',
'maxItems' => 5,
// ...
],
If future requirements justify it, we can always relax the constraints later without worrying about breaking existing consumers.
Output Schemas
1. Anticipate the caller’s needs
It’s always worth asking what the caller is going to do with the outcome of the Ability. We did this in part when we changed our place-order output to return the order instead of just an ID, but let’s theorycraft a bit more. A confirmation page shows the product name and image next to its price, while the admin screen wants a link to the Edit Product screen. At minimum, that requires returning the product_id, but if that’s all we return, then a large order will require the caller to loop through dozens of database calls to fetch the product details.
Assuming our execute_callback already fetches the Rt_Commerce\Product object to compute stock levels and pricing, it’s trivial to return the full product details as well. Each entry in order.items now uses Rt_Commerce\Schema::order_item():
// Rt_Commerce\Schema::order_item()
[
'type' => 'object',
'properties' => [
'product' => [
'type' => 'object',
'properties' => [
'id' => [ 'type' => 'integer' ],
'name' => [ 'type' => 'string' ],
'sku' => [ 'type' => 'string' ],
'url' => [ 'type' => [ 'string', 'null' ], 'format' => 'uri' ],
'image_url' => [ 'type' => [ 'string', 'null' ], 'format' => 'uri' ],
],
'required' => [ 'id', 'name', 'sku', 'url', 'image_url' ],
],
'quantity' => [ 'type' => 'integer' ],
'unit_price' => [
'type' => 'integer',
'description' => __( 'The price per unit at the time of purchase, in the smallest currency unit (e.g. cents).', 'rt-commerce' ),
],
'subtotal' => [ 'type' => 'integer' ],
],
'required' => [ 'product', 'quantity', 'unit_price', 'subtotal' ],
'additionalProperties' => false,
]
Note that the nested product object still includes the id, so a caller that needs to reference the product programmatically for other purposes can just as easily do so without relying on the SKU or other fields that might drift over time.
2. Include what’s been computed
Building on the previous point, some of the most useful information an ability can return isn’t stored anywhere, but is instead information that callers would need to recompute for themselves. For inputs, we noted that if it can be inferred or computed from other fields, it shouldn’t be part of the schema. For output schemas, the inverse is true: anything that we did already calculate is a potential candidate to return.
To place the order, our implementation likely worked out the unit prices, and the effects of coupons and taxes on specific line items. If we hand back that breakdown, then the caller doesn’t need to recompute all those differences for their “You saved ${X} on your order!” message. More importantly, we don’t need to worry about the caller’s computations drifting from our own calculations as we iterate.
'coupons' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'code' => [ 'type' => 'string' ],
'discount' => [
'type' => 'integer',
'description' => __( 'The amount this coupon took off the order, in the smallest currency unit.', 'rt-commerce' ),
],
],
'required' => [ 'code', 'discount' ],
],
],
'shipping' => [
'type' => [ 'object', 'null' ],
'properties' => [
'method' => [ 'type' => 'string' ],
'label' => [ 'type' => 'string' ],
'cost' => [ 'type' => 'integer' ],
],
],
'totals' => [
'type' => 'object',
'properties' => [
'subtotal' => [ 'type' => 'integer' ],
'discount' => [ 'type' => 'integer' ],
'shipping' => [ 'type' => 'integer' ],
'tax' => [ 'type' => 'integer' ],
'total' => [ 'type' => 'integer' ],
'currency' => [ 'type' => 'string', 'enum' => Rt_Commerce\Schema::supported_currencies() ],
],
],
See how coupons.discount now exists alongside the provided coupons.code? That’s also the benefit of our object nesting from earlier. Even if we didn’t ship with this field initially, including it now is an additive, non-breaking change.
3. Return what’s been mutated
Placing an order doesn’t just create an order. It reduces product stock, uses up a coupon redemption, and might create a customer account for a guest. As discussed in Part 2, the best way to handle side effects is to be explicit about them, and the easiest way to do that is to tell the caller what’s changed instead of forcing them to infer it themselves or hit the database with a follow-up query. Taking advantage of our wrapped order, we can return what we need without muddying up the Ability data structure.
// Rt_Commerce\Schema::order_item()
'product' => [
'type' => 'object',
'properties' => [
'id' => [ 'type' => 'integer' ],
//... other fields
+ 'inventory' => [
+ 'type' => 'object',
+ 'properties' => [
+ 'in_stock' => [ 'type' => 'boolean' ],
+ 'remaining' => [
+ 'type' => [ 'integer', 'null' ],
+ 'description' => __( 'Units currently in stock, or null if stock is not tracked for this product.', 'rt-commerce' ),
+ ],
+ ],
+ 'required' => [ 'in_stock', 'remaining' ],
+ 'additionalProperties' => false,
+ ],
],
],
// Rt_Commerce\Schema::order()
'customer' => [
'oneOf' => [
[
'title' => 'Registered customer',
'type' => 'object',
'properties' => [
'type' => [ 'type' => 'string', 'enum' => [ 'user' ] ],
'id' => [ 'type' => 'integer' ],
// ... other fields
+ 'created_by_order' => [
+ 'type' => 'boolean',
+ 'description' => __( 'Whether this customer account was created when the order was placed.', 'rt-commerce' ),
+ ],
],
],
[
'title' => 'Guest',
'type' => 'object',
// ...
],
],
],
4. Use nullability for non-critical fields
Not every field carries the same weight, and while we want to return as much as we can, it’s important to distinguish between what’s critical to the outcome and all the other ancillary information we’re including for completeness or ease-of-use. In our case, the Order and Customer IDs (or email, if guest) are what’s critical, but product, inventory, even the calculated fields are all extra.
Nullability gives us a way to evolve the schema without worrying about breaking existing consumers, since they should already be handling the potential null values. We could even stop populating the field entirely and their implementation wouldn’t break, just have a slightly less rich outcome until they decide to refactor their usage. It provides a way to implement field-level permission control and handle errors and edge cases more gracefully, allowing us to avoid clobbering the entire response into a \WP_Error if a non-critical field encounters an issue.
It pays to make these decisions upfront, and if you’re unsure about whether a field is actually critical to the outcome, err on the side of nullability, as making a non-nullable field nullable later is a breaking change.
5. Know when to stop
This is the most important rule, and the biggest caveat to all the preceding principles: a rich output schema does not mean you should return everything. While the Abilities API is heavily influenced by GraphQL, we don’t have the benefit of per-field resolvers or a global query language that allows clients to request exactly what they need. By default, Abilities return their entire response, so preemptively fetching and building convenience data comes with unnecessary overhead. Even the schema itself comes with a performance cost, as the larger the schema, the more expensive it is to validate, and all those translatable descriptions add up as well.
Extra data should only be included if it’s cheaper for the implementation to produce than userland, and if the information is safe to share.
Modeling Errors and Edge Cases
Our initial create-order forced everything into a false dichotomy of success (an order ID) or false. The redesigned place-order output is much better at describing a successful order, but a caller still needs to know what else can occur – the errors and edge cases – so it can adequately recover and handle them appropriately. Currently, the Abilities API gives us two places to model these outcomes: a \WP_Error, or as part of the output schema itself. Let’s explore how and when to use each effectively.
1. Model every expected outcome
Earlier in this article, our payment.method enum included support for bank_transfer and cash_on_delivery, neither of which requires immediate payment. Combined with multi-step flows like 3-D Secure, a successfully placed order can actually be in one of three states: paid, waiting on the customer to confirm with their bank, or waiting on payment that will arrive later. We addressed that with a payment.status enum and a next_action field, but that ambiguity leaves it up to the implementer to determine which fields should be available at a given time. Is next_action ever set when the order is paid? Where do the bank transfer instructions go?
Using oneOf in the return type allows us to explicitly model different outcomes by making each combination an explicit variant:
'payment' => [
'oneOf' => [
[
'title' => 'Paid',
'type' => 'object',
'properties' => [
'status' => [ 'type' => 'string', 'enum' => [ 'paid' ] ],
'method' => [ 'type' => 'string' ],
],
'required' => [ 'status', 'method' ],
'additionalProperties' => false,
],
[
'title' => 'Requires customer action',
'type' => 'object',
'properties' => [
'status' => [ 'type' => 'string', 'enum' => [ 'requires_action' ] ],
'next_action' => [
'type' => 'object',
'properties' => [
'type' => [ 'type' => 'string', 'enum' => [ 'redirect' ] ],
'url' => [ 'type' => 'string', 'format' => 'uri' ],
],
'required' => [ 'type', 'url' ],
],
],
'required' => [ 'status', 'next_action' ],
'additionalProperties' => false,
],
[
'title' => 'Awaiting payment',
'type' => 'object',
'properties' => [
'status' => [ 'type' => 'string', 'enum' => [ 'awaiting_payment' ] ],
'method' => [ 'type' => 'string', 'enum' => [ 'bank_transfer', 'cash_on_delivery' ] ],
'instructions' => [
'type' => 'string',
'description' => __( 'What the customer needs to do to complete payment, e.g. bank details and a payment reference.', 'rt-commerce' ),
],
],
'required' => [ 'status', 'method', 'instructions' ],
'additionalProperties' => false,
],
],
],
By using the status field to demarcate the different payment outcomes, we know exactly which fields are relevant for each scenario and can make sure we’re handling each case correctly.
2. Be consistent with WP_Error
Just because \WP_Error isn’t part of the schema doesn’t mean we can’t give its internals some consistency. Treating errors as part of your structured contract allows consumers to parse, handle, and even programmatically recover from them before the end user notices.
You can do this by using a fixed set of error codes for each ability, and a consistent data shape of the additional information needed to handle it. Where possible, make that shape match the schema. For example, if insufficient_stock reports items using the same product_id and quantity fields as the input, the caller can find the offending items in what they sent and fix them:
new \WP_Error(
'insufficient_stock',
__( 'Some items are not available in the requested quantity.', 'rt-commerce' ),
[
'items' => [
[ 'product_id' => 42, 'quantity' => 3, 'available' => 1 ],
],
]
);
The code and the data that accompany a \WP_Error should be treated as programmatic, with changes to either shape considered a breaking change. The message, however, is meant for human – or agentic – consumption and can be updated more freely to improve clarity or guidance. It’s also good practice to list the possible error codes in the Ability description where humans and agents can discover them:
'description' => __(
'Places an order and charges the customer. ... Possible errors: insufficient_stock, invalid_coupon, payment_declined.',
'rt-commerce'
),
Keep the list of errors to those that a user can do something specific about. A database timeout or a payment provider outage can be given a more generic error code, like temporary_failure, since remediation (“…Please try again later”) is the same, with the specifics shared in the data payload or written to your debug.log file.
3. Make “nothing” a valid response
Empty results are easy to get wrong, and a common antipattern is to treat “nothing found” as an error. In reality, it’s important to differentiate between an expected empty result and one caused by an actual failure, and the schema should use different falsy values for each.
In general, it makes sense to use falsy values of the same datatype as a successful one. For lists, using an empty [] to indicate, for example, that no products are attached to the order allows consumers to iterate through them without a special typecheck. For single values, it will vary on the field. For example, if our order had an optional customer_note field, using '' would make it clear that no note was provided, versus a null that could be the result of a failure to retrieve it, invalid permissions, or some other error.
It also pays to be consistent in our treatment of null, and be explicit as to whether there’s no value, the value is explicitly null or unset, or the value is null because something went wrong. Here’s how that could look on our inventory field:
'inventory' => [
'description' => __( 'Current stock for this product.', 'rt-commerce' ),
'oneOf' => [
[
'title' => 'Tracked',
'type' => 'object',
'properties' => [
'status' => [ 'type' => 'string', 'enum' => [ 'tracked' ] ],
'in_stock' => [ 'type' => 'boolean' ],
'remaining' => [ 'type' => 'integer', 'minimum' => 0 ],
],
'required' => [ 'status', 'in_stock', 'remaining' ],
'additionalProperties' => false,
],
[
'title' => 'Not tracked',
'type' => 'object',
'properties' => [
'status' => [ 'type' => 'string', 'enum' => [ 'not_tracked' ] ],
'in_stock' => [ 'type' => 'boolean' ],
],
'required' => [ 'status', 'in_stock' ],
'additionalProperties' => false,
],
[
'title' => 'Unknown',
'type' => 'object',
'properties' => [
'status' => [ 'type' => 'string', 'enum' => [ 'unknown' ] ],
'in_stock' => [ 'type' => 'null' ],
'remaining' => [ 'type' => 'null' ],
],
'required' => [ 'status', 'in_stock', 'remaining' ],
'additionalProperties' => false,
],
[ 'type' => 'null' ],
],
],
Through the use of oneOf and explicit nullability, we now have both the success and failure paths explicitly mapped. A sold-out product is one whose inventory status is tracked, but has in_stock: false and a remaining: 0, while products that don’t track inventory levels do away with the remaining field altogether. Issues in tracking inventory – for example, if the warehouse’s ERP is down – are explicitly modeled as unknown, making it clear that those null values for in_stock and remaining are temporary API issues.
4. Handle partial success with an errors object
In the previous example, we used inventory.status: unknown to indicate something went wrong, but we didn’t say what went wrong. Further, by inlining partial successes as part of the schema, we forced consumers to traverse the schema to determine whether something went wrong at all. That might work in some cases, but the larger the schema is, the more brittle all those branching paths may become.
Here too, we can borrow a pattern from GraphQL and return a top-level errors field. Because we wrapped our order in an object, we have room to do the same thing:
'output_schema' => [
'type' => 'object',
'properties' => [
'order' => Rt_Commerce\Schema::order(),
'errors' => [
'type' => 'array',
'description' => __( 'Problems that did not prevent the order from being placed, but may have left the fields at each error\'s path incomplete. Empty if there were none.', 'rt-commerce' ),
'items' => [
'type' => 'object',
'properties' => [
'code' => [ 'type' => 'string', 'enum' => Rt_Commerce\Schema::order_error_codes() ],
'message' => [ 'type' => 'string' ],
'path' => [ 'type' => 'array', 'items' => [ 'type' => [ 'string', 'integer' ] ] ],
'data' => [ 'type' => [ 'object', 'null' ] ],
],
'required' => [ 'code', 'message', 'path', 'data' ],
'additionalProperties' => false,
],
],
],
'required' => [ 'order', 'errors' ],
'additionalProperties' => false,
],
By sharing a similar shape as \WP_Error but without blocking the rest of the response, we’re now able to return as many errors as we need, along with remediation suggestions and additional context for each one. So if the warehouse’s ERP is down, we can send back:
[
'order' => [
'id' => 1234,
'items' => [
[
'product' => [
'id' => 42,
// ...
'inventory' => [ 'status' => 'unknown', 'in_stock' => null, 'remaining' => null ],
],
// ...
],
],
// ...
],
'errors' => [
[
'code' => 'temporary_failure',
'message' => 'Stock levels could not be retrieved from the warehouse. Try again later.',
'path' => [ 'order', 'items', 0, 'product', 'inventory' ],
'data' => null,
],
],
]
If we have multiple errors we can stack them:
'errors' => [
[
'code' => 'temporary_failure',
'message' => 'Stock levels could not be retrieved from the warehouse. Try again later.',
'path' => [ 'order', 'items', 0, 'product', 'inventory' ],
'data' => null,
],
[
'code' => 'unsupported_card_type',
'message' => 'The provided card type is not supported. The order is on hold until a different payment method is provided.',
'path' => [ 'order', 'payment', 'method' ],
'data' => [ 'card_type' => 'diners', 'allowed_card_types' => [ 'visa', 'mastercard', 'amex' ] ],
],
[
'code' => 'tax_estimated',
'message' => 'The tax provider could not be reached, so tax was calculated with a fallback rate and may be adjusted.',
'path' => [ 'order', 'totals', 'tax' ],
'data' => [ 'fallback_rate' => 0.08 ],
],
],
By following a similar shape to what we use for \WP_Error we make it easy for callers to handle these errors with the same logic, while still keeping \WP_Error reserved for issues that are truly unrecoverable.
Evolving your schema (without breaking existing consumers)
Until now, we’ve been focused on designing schemas with future compatibility in mind. Inevitably, though, we’ll need to evolve our schema, so let’s see how we can do so additively and without breaking our existing contracts.
First, let’s talk about what constitutes a breaking change. We’ve touched on most of these in passing, so if you’ve gotten this far they should be familiar to you already.
For inputs:
- Adding a required field (including making an optional field required).
- Tightening a constraint (e.g. lowering
coupons.maxItems, or removing a value from one of ourenums). - Renaming or removing a field.
- Narrowing or changing a field’s type.
For outputs:
- Loosening a constraint, such as adding an `enum` value or increasing `maxItems`.
- Renaming or removing a field.
- Changing what a field means, even if its shape stays the same.
- Broadening a field’s type, including making a non-nullable field nullable.
For WP_Error:
- Changing an error code, or the shape of the
data.
Meanwhile, the list of safe changes is much shorter:
- Adding optional input fields, and broadening or loosening input constraints.
- Adding new output fields, and tightening output constraints.
Since removing a field isn’t backwards-compatible, the best approach is to add new fields alongside the old one, and have our implementation handle the coercion internally. (By nesting, we’ve already made space for it.)
Take our product_id field as an example, which requires the consumer to have done a previous lookup to resolve it. If we want to widen it to support SKUs, slugs, or other unique identifiers, we can’t just widen the type of product_id to string | integer without losing semantic meaning, and making it nullable and adding other optional input fields alongside also creates ambiguity just on the consumer side. From a broader perspective, we want the same pattern we used for customer, a oneOf that lets you place an order “by ID” or “by SKU”, etc. That, however, requires an object:
'items' => [
'type' => 'array',
'minItems' => 1,
'maxItems' => 100,
'items' => [
'type' => 'object',
'properties' => [
- 'product_id' => [ 'type' => 'integer', 'minimum' => 1 ],
+ 'product_id' => [
+ 'type' => 'integer',
+ 'minimum' => 1,
+ 'description' => __( 'Deprecated: use `product.id` instead. Ignored when `product` is set.', 'rt-commerce' ),
+ ],
+ 'product' => [
+ 'description' => __( 'The product to order, identified by either its ID or its SKU.', 'rt-commerce' ),
+ 'oneOf' => [
+ [
+ 'title' => 'By ID',
+ 'type' => 'object',
+ 'properties' => [
+ 'id' => [ 'type' => 'integer', 'minimum' => 1 ],
+ ],
+ 'required' => [ 'id' ],
+ 'additionalProperties' => false,
+ ],
+ [
+ 'title' => 'By SKU',
+ 'type' => 'object',
+ 'properties' => [
+ 'sku' => [ 'type' => 'string', 'minLength' => 1 ],
+ ],
+ 'required' => [ 'sku' ],
+ 'additionalProperties' => false,
+ ],
+ // Other identifiers like slugs, URI, etc. can be added here as additional `oneOf` branches.
+ ],
+ ],
'quantity' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 999 ],
],
- 'required' => [ 'product_id', 'quantity' ],
+ 'required' => [ 'quantity' ],
+ // Either `product` or the deprecated `product_id` must still be set.
+ 'anyOf' => [
+ [ 'type' => 'object', 'required' => [ 'product' ] ],
+ [ 'type' => 'object', 'required' => [ 'product_id' ] ],
+ ],
'additionalProperties' => false,
],
],
With product_id no longer required, we encourage consumers to use the new product object, and coerce their existing integrations in our execute_callback:
'execute_callback' => static function ( array $input ) {
foreach ( $input['items'] as $i => $item ) {
if ( isset( $item['product_id'] ) ) {
_deprecated_argument( 'rt-commerce/place-order', '2.1.0', __( 'Use `items[].product.id` instead of `items[].product_id`.', 'rt-commerce' ) );
$input['items'][ $i ]['product'] ??= [ 'id' => $item['product_id'] ];
unset( $input['items'][ $i ]['product_id'] );
}
}
// From here, the implementation only needs to handle `items[].product`
// ...
},
Output fields can follow the same principle. Callers can’t pick which fields are explicitly returned like in GraphQL, but we can still DRY out our code so the deprecated fields are derived from the new ones:
// Rt_Commerce\Schema::order()
'shipping' => [
'type' => [ 'object', 'null' ],
'description' => __( 'Deprecated: use `fulfillment` instead. Null unless the order is being shipped.', 'rt-commerce' ),
// ...unchanged.
],
'fulfillment' => [
'oneOf' => [
[ /* method: 'shipping', rate: the same { method, label, cost } shape as `shipping` */ ],
[ /* method: 'pickup', location: Rt_Commerce\Schema::address() */ ],
[ /* method: 'none' */ ],
],
],
// Inside `execute_callback` :
$order['shipping'] = 'shipping' === $order['fulfillment']['method']
? $order['fulfillment']['rate']
: null;
In some cases, it may make sense to phase out the deprecated field entirely because the shape is too different. For optional fields that we previously designed to support null, we can just always return null for those fields until they are officially removed.
Note: As of WordPress 7.1, all of the deprecation work is manual. There are proposals to add built-in support for handling ability and field deprecation in the future.
Deprecating Abilities
We’ve spent a lot of time discussing how to structure your schemas in a way to avoid breaking changes as your API evolves. Sometimes, however, you have no other choice but to deprecate an Ability to start fresh.
Before we go into the specifics of how to deprecate an Ability, it’s important to understand why deprecation is a last resort. Say we’ve deprecated create-order in favor of place-order. Unlike a deprecated field that’s limited to a specific part of the schema, a deprecated Ability stays in the global registry, and more importantly, in the minds of consumers and the training data of LLMs. Here’s roughly what an MCP client sees when it asks our site what it can do:
rt-commerce/create-order Create a new order.
rt-commerce/place-order Places an order and charges the customer.
By default, a consumer doesn’t know that create-order is deprecated until after it’s used. That means agents are likely to reach for it, and if it’s been around for a while, it’ll stay their default unless you’re adding extra context to tell them to avoid it. And, in addition to wasting tokens, it’s also now something that needs to be maintained to avoid drift.
Versioning multiplies these problems. Say instead of place-order, we introduce a place-order-v2 or -v3:
rt-commerce/place-order Places an order and charges the customer.
rt-commerce/place-order-v2 Places an order with improved validation.
rt-commerce/place-order-v3 Places an order with even more improvements.
That’s not just three abilities we need to juggle, but since the existence of a v3 implies the existence of a v4, the agent now has to be aware of a potentially unbounded sequence of versions, each of which may have subtle differences and require maintenance.
WordPress core is working to make Ability deprecation a first-class concept, but until then, here’s how to handle ability deprecation when schema evolution is no longer sufficient.
Handling Ability Deprecation
In practice, the approach follows similar principles to how we handled field deprecation: add the replacement alongside the original, mark the original as deprecated in the description and with a _deprecated_*() function, and have the old ability consume the new one internally, whenever possible, to minimize drift and keep your codebase maintainable. Say we had already shipped create-order before reading this article and redesigning it as place-order. Here’s how we might handle it:
wp_register_ability(
'rt-commerce/create-order',
[
'label' => __( 'Create Order', 'rt-commerce' ),
'description' => __( 'Deprecated: use rt-commerce/place-order instead, which returns the full order. Creates a new order and charges the customer.', 'rt-commerce' ),
'category' => 'ecommerce',
// ...the original input_schema, output_schema and permission_callback.
'execute_callback' => static function ( array $input ) {
_deprecated_function( 'rt-commerce/create-order', '2.0.0', 'rt-commerce/place-order' );
$result = wp_get_ability( 'rt-commerce/place-order' )->execute( [
'customer' => isset( $input['user_id'] )
? [ 'user_id' => $input['user_id'] ]
: [ 'email' => $input['billing_email'] ],
'items' => array_map(
static fn ( $product_id, $quantity ) => [
'product' => [ 'id' => $product_id ],
'quantity' => $quantity ?? 1,
],
$input['products'],
$input['quantities'] ?? []
),
'payment' => [ 'method' => 'card', 'token' => $input['stripe_token'] ],
// ...and the coupon, billing_* and shipping_* fields.
] );
// The old contract: an order ID if the customer was charged, false otherwise.
if ( is_wp_error( $result ) || 'paid' !== $result['order']['payment']['status'] ) {
return false;
}
return $result['order']['id'];
},
]
);
From the outside, nothing has changed for anyone calling the existing create-order Ability. Internally, however, by wrapping the new place-order Ability, any bug fixes or implementation details in the “new” ability are automatically applied to the old one, which is now only responsible for coercing the old schema shapes to the new format.
Additionally, by placing a deprecation message in both the description and the execute_callback, both developers and agentic consumers are clearly informed that this is not the preferred method for interacting with the site, and they should migrate to using rt-commerce/place-order instead.
Next Time: Scaling with Abilities
In this article, we covered the essentials of API design, including what makes a good input and output schema, and how to design schemas that are ready for immediate consumption while still leaving room for future, non-breaking evolution. We learned how to model errors and edge cases as part of the schema itself, and how to deprecate schema fields and even entire abilities as a last resort.
In the next article, we’ll explore strategies and patterns that come up when using Abilities at scale. We’ll cover:
- Chaining Abilities together to create complex workflows
- Structuring your codebases around Ability-first composition
- Handling asynchronous, long-running, and multi-step abilities
- Performance considerations and strategies to avoid bottlenecks and overfetching
And other best practices for building and maintaining enterprise-grade Abilities.
See you next time!
Part 2: Building with Abilities
PREVIOUS
Credits
David Levine
Author
David Levine
Author
David Levine is a Senior WordPress Engineer and Product Lead at rtCamp with a background that few engineers in the WordPress space can claim. A WordPress Core Contributor across multiple releas…
Aviral Mittal
Editor
Aviral Mittal
Editor
Aviral Mittal is the Chief Marketing Officer at rtCamp, where he established and leads the marketing function, building and growing a team of 20+ specialists across content, SEO, design, and growth…
Good Work. Good People.
Industry partnerships
Compliance certifications
United States
India
© rtCamp Inc. since 2009. All rights reserved.
Terms of Service · Privacy Policy · Trust Center
Company
Solutions
Subscribe to Campfire for fresh insights and stories from people behind rtCamp
Δ
Email(Required)
Submit
United States
India
© rtCamp Inc. since 2009. All rights reserved.
Terms of Service · Privacy Policy · Trust Center
Cookie Consent
We value your privacy
We use cookies to give you the best possible experience. By clicking “Accept,” you consent to our use of cookies to improve site functionality, analyze usage, and personalize content and communications. Your privacy matters to us, and we are committed to handling your data responsibly and transparently. Please check our Privacy Policy for more details.
Manage PreferencesDon’t AllowAllow All
Why do we use cookies?
×
By clicking "Accept" or "Decline All" at the bottom, you consent to the use of cookies and other tools as described in our Cookie Policy in accordance with your settings and accept our Terms of Service.
Toggle EssentialEssential
Essential cookies enable basic functions and are necessary for the proper function of the website.
Name
Description
Duration
Geolocation Config
This cookie is used to store the consent settings based on the visitor's location.
30 days
Cookie Preferences
This cookie is used to store the user's cookie consent preferences.
30 days
Toggle CloudFlareCloudFlare
CloudFlare provides web performance and security solutions, enhancing site speed and protecting against threats.
Service URL: developers.cloudflare.com (opens in a new window)
Name
Description
Duration
cf_clearance
Whether a CAPTCHA or Javascript challenge has been solved.
session
Toggle CommentsComments
These cookies are needed for adding comments on this website.
Name
Description
Duration
comment_author
Used to track the user across multiple sessions.
Session
comment_author_email
Used to track the user across multiple sessions.
Session
comment_author_url
Used to track the user across multiple sessions.
Session
Toggle GodamGodam
GoDAM" is primarily a specialized WordPress plugin and media management service designed to enhance video hosting, marketing, and asset management directly within the WordPress dashboard.
Service URL: godam.io (opens in a new window)
Name
Description
Duration
user_image
Temporarily stores the path to the user's avatar or profile picture for quick rendering in the website header.
session
user_id
Stores the numerical ID of the logged-in user to maintain session continuity and basic site operations.
session
full_name
Stores the logged-in user's display name to personalize the site interface without needing database queries.
session
system_user
First-party cookie used to store basic application state identifying the current system user role.
session
sid
A generic session ID cookie used to maintain user state and functionality as the visitor navigates through the site.
session
Toggle Google reCAPTCHAGoogle reCAPTCHA
Google reCAPTCHA helps protect websites from spam and abuse by verifying user interactions through challenges.
Name
Description
Duration
_GRECAPTCHA
Google reCAPTCHA sets a necessary cookie (_GRECAPTCHA) when executed for the purpose of providing its risk analysis.
179 days
Toggle Google Tag ManagerGoogle Tag Manager
Google Tag Manager simplifies the management of marketing tags on your website without code changes.
Name
Description
Duration
cookiePreferences
Registers cookie preferences of a user
2 years
td
Registers statistical data on users' behaviour on the website. Used for internal analytics by the website operator.
session
Toggle StatisticsStatistics
Statistics cookies collect information anonymously. This information helps us understand how visitors use our website.
Toggle Factors AIFactors AI
Factors.ai is a B2B account intelligence and marketing analytics platform that helps Go-To-Market (GTM) teams identify anonymous website visitors, track buyer journeys, and measure the ROI of marketing campaigns.
Service URL: www.factors.ai (opens in a new window)
Name
Description
Duration
_fuid
It is sent to capture session details and track user behavior across your website to provide behavioral data and intent signals.
1 Year
Toggle Google AnalyticsGoogle Analytics
Google Analytics is a powerful tool that tracks and analyzes website traffic for informed marketing decisions.
Service URL: policies.google.com (opens in a new window)
Name
Description
Duration
FPGSID
Stores a session or user identifier to track how visitors interact with a website. This helps Google Analytics measure website performance, user engagement, and usage patterns.
Session
FPLC
Used by Google Analytics to link visitor interactions and sessions across multiple related domains.
20 hours
FPID
A server-side Google Analytics cookie used as an alternative user identifier when third-party cookies are restricted.
2 years
_ga
ID used to identify users
2 years
_ga_
ID used to identify users
2 years
Toggle Jetpack StatsJetpack Stats
Jetpack's built-in visitor analytics. It records page views, referring sites, search terms, and outbound link clicks, and also carries the shared visitor-tracking library used by Jetpack Instant Search and WooCommerce Analytics.
Service URL: automattic.com (opens in a new window)
Name
Description
Duration
tk_aip
Stores a list of anonymous visitor IDs so they can be merged into one identity once a visitor is recognized.
Up to 5 years
tk_tc
Used once per page load to work out which cookie domain the Tracks library should use, then removed as soon as it's read back.
Session (deleted immediately after use)
tk_qs
Queues analytics events for Jetpack's Tracks library so none are lost if the page closes before they can be sent.
30 minutes
tk_ai
Stores a randomly-generated anonymous visitor ID so Jetpack's Tracks analytics library can link tracking events to the same visitor.
Session in wp-admin; up to 5 years on the frontend
Toggle Microsoft ClarityMicrosoft Clarity
Clarity is a web analytics service that tracks and reports website traffic.
Service URL: clarity.microsoft.com (opens in a new window)
Name
Description
Duration
CLID
Identifies the first-time Clarity saw this user on any site using Clarity.
12 months
ANONCHK
Indicates whether MUID is transferred to ANID, a cookie used for advertising. Clarity doesn't use ANID and so this is always set to 0.
Session
_clck
Persists the Clarity User ID and preferences, unique to that site is attributed to the same user ID.
12 months
_clsk
Connects multiple page views by a user into a single Clarity session recording.
12 months
Toggle Parse.lyParse.ly
Parse.ly is a content analytics platform that helps publishers optimize audience engagement and content performance.
Name
Description
Duration
_parsely_session
JSON document storing information identifying a browsing session according to Parsely’s proprietary definition
30 minutes
_parsely_visitor
JSON document uniquely identifying a browser and counting its sessions
13 months
cookies.js_dtest
This cookie determines whether the browser accepts cookies.
session
Toggle MarketingMarketing
Marketing cookies are used to follow visitors to websites. The intention is to show ads that are relevant and engaging to the individual user.
Toggle Bing / MicrosoftBing / Microsoft
Bing, powered by Microsoft, is a search engine providing web, image, video, and map search capabilities.
Name
Description
Duration
MR
Used to collect information for analytics purposes.
6 months
ANONCHK
Used to store session ID for a users session to ensure that clicks from adverts on the Bing search engine are verified for reporting purposes and for personalisation
10 minutes
SM
Used by Microsoft in synchronizing the MUID across multiple Microsoft domains to track users for advertising.
session
MUID
Identifies unique web browsers visiting Microsoft sites. These cookies are used for advertising, site analytics, and other operational purposes.
1 year
Toggle DoubleClick/Google MarketingDoubleClick/Google Marketing
A comprehensive digital advertising platform for managing campaigns, optimizing performance, and analyzing audience data.
Name
Description
Duration
IDE
This cookie is used for targeting, analyzing and optimisation of ad campaigns in DoubleClick/Google Marketing Suite
2 years
ar_debug
Store and track conversions
Persistent
Toggle LinkedInLinkedIn
LinkedIn is a professional networking platform for job seekers, employers, and industry connections.
Name
Description
Duration
bscookie
Used by LinkedIn to track the use of embedded services.
1 year
AnalyticsSyncHistory
Used to store information about the time a sync with the lms_analytics cookie took place for users in the Designated Countries
30 days
bcookie
Used by LinkedIn to track the use of embedded services.
1 year
li_sugr
Used to make a probabilistic match of a user's identity outside the Designated Countries
90 days
lidc
Used by the social networking service, LinkedIn, for tracking the use of embedded services.
1 day
UserMatchHistory
Used by LinkedIn Ads to synchronize and match user IDs across different ad networks and data providers.
30 days
Toggle LinkedIn InsightLinkedIn Insight
LinkedIn Insight is a web analytics service that tracks and reports website traffic.
Service URL: www.linkedin.com (opens in a new window)
Name
Description
Duration
li_sugr
Used to make a probabilistic match of a user's identity.
90 days
lidc
Used for routing and session management.
24 hours
Toggle LiveIntentLiveIntent
LiveIntent provides a platform for email advertising and identity-driven marketing solutions.
Name
Description
Duration
_lc2_fpi_js
Companion cookie to _lc2_fpi used by JavaScript to facilitate cross-domain ad tracking and user identification.
1 year
_lc2_fpi
First-party tracking cookie usually associated with LiveRamp to identify users across devices for targeted advertising.
1 Year
_li_ss
Sets a unique ID for the visitor, that allows third party advertisers to target the visitor with relevant advertisement. This pairing service is provided by third party advertisement hubs, which facilitates real-time bidding for advertisers.
1 month
lidid
Collects data on visitors' behaviour and interaction - This is used to make advertisement on the website more relevant. The cookie also allows the website to detect any referrals from other websites.
2 years
Toggle Cookie PolicyCookie Policy
You can find more information in our Privacy Policy.
Allow AllDecline All
Accept





