feat: Introduce Actions and apply it to Checkout#553
Conversation
aneeshali
left a comment
There was a problem hiding this comment.
Thanks a lot @raginpirate for introducing the base changes separately. Left a few minor comments!
drewolson-google
left a comment
There was a problem hiding this comment.
Overall looks good, just one comment about ID uniqueness.
| Businesses **MUST** follow these rules: | ||
|
|
||
| * `id` values **MUST** be unique among outstanding actions for the containing | ||
| entity. |
There was a problem hiding this comment.
Do IDs have to be globally unique or just unique within a given action code? Said another way, are IDs automatically namespaced by code?
There was a problem hiding this comment.
Good clarifying question. I propose having them be unique to the entity they are surfaced under, not unique within a given action code. You should not risk collisions between your 3ds action and your ddc action in id.
jamesandersen
left a comment
There was a problem hiding this comment.
Overall this is looking pretty solid
afc1b74 to
6ad7efe
Compare
aneeshali
left a comment
There was a problem hiding this comment.
Thanks a lot Daniel, I think the doc update covering order of resolution is good to begin with. The changes look good overall.
|
We've been building against exactly the gap this PR names in its "known concern": an out-of-band human approval step for unattended buying agents. We shipped an open-source gateway (
All three currently collapse into As implementer feedback, here is the concrete action spec we would emit, structured per the owner obligations in
|
|
@amitpaz1 interesting and valuable to see a use case that further validates the modeling of this base actions PR Thanks @raginpirate for the work here! LGTM |
|
Approving this. Scoping it as a base envelope first is the right call, and it matches #517's own risk framing (Decision 1: "adding a config field later is non-breaking; un-hoisting an envelope field is breaking, so deferring is strictly lower risk"). The primitive is clean, forward-compatible, and the
The one thing I would ask the follow-up to be explicit about is the completion of invariant 11: when the stacked 3DS/action spec lands, it should name the PSP/handler (not the business) as the trust anchor for the executable surface and define how the platform validates that origin independently of the business, so the advisory-outcome class does not end up trusting a business-asserted URL. Non-blocking for this PR. |
igrigorik
left a comment
There was a problem hiding this comment.
@raginpirate @drewolson-google per the TC chat, ptal at this shape: #582.
I won't repeat what's there, but the key thing I'm looking for is clear integration and interop with other and existing primitive in UCP. To that end, I think negotiation/discovery is integral -- the business can emit an action, but how does it know if the platform can action it? I think the answer is simple: actions are extensions; extensions register actions.
Negotiating an extension confirms support for its complete Action contract, but it does not make every future config value or delegate trustworthy. Without an explicit boundary, open config could be mistaken for executable instructions and surface completion for a Business result. Require Platforms to process executable behavior only as defined by the active Action-type contract, allow stricter runtime policy and instance rejection, and keep later Business resource state authoritative. Carry #553's concrete security and fallback concerns into conditional Action-type authoring guidance. Extension specifications define executable fields, trust anchors, interaction controls, completion observation, failure behavior, and fallback only where relevant. This preserves the existing wire shape without introducing a generic executor, callback, or state model.
|
Closing this PR in favour of #582 which delivers the core of this PR and improves on how lifecycle and negotiation were proposed. Thanks Ilya! |
* feat: introduce Actions as a core UCP concept
Add Actions as the machine-readable representation of outstanding work that a
Platform can perform while a Cart or Checkout progresses. Actions are
response-only, keyed by the active extension that defines their behavior, and
carry a small common instance envelope: a stable ID, whether the instance is
required, and optional extension-owned configuration.
For example, a student-verification extension can return an Action alongside a
provisional discount and an explanatory Message:
{
"actions": {
"com.example.identity.student_verification": [{
"id": "verify-student-1",
"required": true,
"config": {
"verification_url": "https://business.example.com/verify/abc"
}
}]
},
"messages": [{
"type": "info",
"code": "eligibility_accepted",
"content": "Student discount applied provisionally. Verify your status."
}]
}
The Action identifies the outstanding work and provides its structured
configuration. Messages remain orthogonal: they explain resource state and
provide buyer-facing context, but do not define how the Action is processed or
determine its outcome.
Integrate Actions with the core Cart and Checkout models. Cart remains
statusless and permits unrelated mutations while an Action is outstanding.
Checkout uses its existing lifecycle: required Actions can keep a checkout
incomplete, ready-for-complete checkouts have none, and Complete Checkout can
return complete_in_progress with a required Action and no order. After
processing it, the Platform polls Get Checkout for the authoritative result
rather than issuing another Complete request.
Represent each response as a complete snapshot of outstanding Action instances.
Keep IDs stable for the same obligation, assign replacements new IDs, and let
each defining extension own configuration, processing, trust, fallback, and
outcome semantics.
Reuse existing capability negotiation and allOf composition so concrete
extensions register and validate their own Action keys without introducing a
separate Action registry, execution protocol, or generic state machine.
* docs: define Action keys as extension-declared types
The original Actions contract equated each map key with the extension that
defined it. That framing implied a one-to-one relationship between extensions
and Actions, making it unclear how one negotiated extension could expose
several distinct runtime contracts such as device-data collection and a 3DS
challenge.
Define each reverse-domain map key as a concrete Action type declared by an
active extension. An extension may use its own name for a single Action type or
declare several distinct types within a namespace controlled by its schema
authority. Negotiating the extension activates the complete contract it
declares.
For example, one `com.example.payment.authentication` extension can declare
two independently dispatched Action types:
{
"actions": {
"com.example.payment.authentication.device_data_collection": [{
"id": "collect-device-data-1",
"required": true
}],
"com.example.payment.authentication.three_ds_challenge": [{
"id": "challenge-cardholder-1",
"required": true
}]
}
}
This preserves direct dispatch and homogeneous instance arrays without adding
a per-instance type discriminator. Existing allOf composition lets the
declaring extension contribute each type key and validate its config, while
existing reverse-domain governance provides namespace ownership without an
Action-specific registry.
Clarify that the map defines no processing order across Action types. JSON
preserves order within each type's array, but the declaring extension decides
whether that order has processing meaning and defines any cross-type
sequencing.
Align Cart and Checkout terminology around Action-defined gated effects and
update the Student Verification example to show that an extension may use its
own name for its single Action type.
This changes the semantic definition, not the wire shape: Action instances
remain response-only objects containing id, required, and optional config;
non-empty arrays, stable identity, parent lifecycle, and idempotency rules are
unchanged.
* docs: correlate operation failures with Actions
A required Action describes a gate on the effect defined by its Action type,
but it does not say whether a particular operation was accepted. Adding a
separate blocking severity would conflate those dimensions: the same Action can
remain required while also explaining why one attempted effect was not applied.
Use the existing Message contract to express that relationship. A Business
returns the current resource with a recoverable error Message whose RFC 9535
path selects the related Action occurrence. Info and warning Messages may use
the same path relationship to explain outstanding work without reporting an
operation failure.
For example:
{
"status": "incomplete",
"actions": {
"com.example.identity.student_verification": [{
"id": "verify-student-1",
"required": true,
"config": {
"verification_url": "https://business.example.com/verify/abc"
}
}]
},
"messages": [{
"type": "error",
"code": "eligibility_verification_required",
"severity": "recoverable",
"path": "$.actions['com.example.identity.student_verification'][0]",
"content": "Verification is required before this effect can be applied."
}]
}
This keeps the signals orthogonal: the Action defines outstanding work and
requiredness, the Message reports the response-scoped operation outcome, and
the parent resource status remains authoritative for lifecycle.
Clarify the Checkout consequences. If a required Action prevents Complete
Checkout from being accepted, the Business returns the current Checkout with
status incomplete and a recoverable error pointing to the Action. A
complete_in_progress response instead means Complete Checkout was accepted for
asynchronous processing, including when that accepted flow waits for a required
Action.
Processing an Action does not automatically replay the earlier request. The
Platform obtains the latest Checkout through Get Checkout or a subsequent
Update response, and any later re-drive is a new operation governed by the
existing idempotency rules.
Broaden recoverable errors to include conditions resolved through a related
Action, not only input modification. Keep Message paths as ordinary positional
RFC 9535 locations in the returned response without adding naming rules,
stable-index guarantees, or an Action-specific Message type.
The wire shape remains unchanged: Actions still contain id, required, and
optional config, and Messages retain their existing type, code, severity,
content, and optional path fields.
* docs: define Action trust and execution boundaries
Negotiating an extension confirms support for its complete Action contract,
but it does not make every future config value or delegate trustworthy.
Without an explicit boundary, open config could be mistaken for executable
instructions and surface completion for a Business result.
Require Platforms to process executable behavior only as defined by the active
Action-type contract, allow stricter runtime policy and instance rejection, and
keep later Business resource state authoritative.
Carry #553's concrete security and fallback concerns into conditional
Action-type authoring guidance. Extension specifications define executable
fields, trust anchors, interaction controls, completion observation, failure
behavior, and fallback only where relevant. This preserves the existing wire
shape without introducing a generic executor, callback, or state model.
* adopt actions in Catalog responses
Expose response-only Actions on Catalog Search, batch Lookup, and
successful Get Product responses so negotiated extensions can gate or
explain catalog effects without introducing a Catalog workflow.
Keep existing product payload requirements intact, allow Businesses to
return zero, some, or all otherwise relevant results, and require a fresh
Catalog operation after Action processing. Clarify the shared Action,
Message, identity, sequencing, and trust rules while preserving Checkout
as the lifecycle authority.
* define Checkout Action recovery
An Action can report completion before the Business reflects its result,
leaving the Platform at risk of repeating external work, resubmitting
Complete Checkout, or checking state indefinitely.
Make Business-reported Checkout state authoritative: use bounded Get
Checkout requests through expires_at, prevent unsafe Action retries, and
order fallback, continue_url handoff, and cancellation. Extend the
authoring guide so concrete Action contracts define timeout, backoff,
safe-retry, and failure behavior without adding generic polling fields.
* clarify Action identity scope
Catalog operations do not share a resource lifetime, making the common
cross-response ID rule ambiguous.
Define uniqueness within each response, retain stable IDs across
representations of the same parent resource, and allow independent Catalog
calls to reuse IDs without implying correlation.
* simplify Action gating and freeze Checkout updates
The per-instance required flag did not represent a distinction used by any
current Action and made gating easy to conflate with Message severity.
Accepted asynchronous completion also left Update availability ambiguous,
risking a full replacement racing Business order processing.
Make every emitted Action gate its Action-type-defined effect, leave Messages
to explain responses and operation outcomes, and remove required from the
common schema and adopting capability examples.
Treat complete_in_progress as frozen for new Update and Complete operations.
Keep Get Checkout authoritative, preserve replay-protected duplicate responses,
and align the core, REST, MCP, OpenAPI, and OpenRPC contracts.
Description
Introduce the base Actions primitive for UCP. Actions are scoped, in-band instructions emitted during the lifecycle of a UCP entity when the platform needs to perform an additional step before the entity can progress. The base primitive is intentionally small:
id— stable correlation identifier for an action occurrencecode— reverse-domain action codeseverity—optional,required, orblockingconfig— owner-defined action configurationThis PR also wires actions into Checkout responses through a top-level
actions[]field and documents how actions interact with the checkout lifecycle.The design invariants for this solution are well defined in #517
What this intentionally does not take from #517
Issue #517 proposes a richer Option A execution contract for step-up actions. This PR deliberately does not include those fields in the base primitive.
Specifically, this PR does not add:
stateexpires_atmodeexecutionresultallowed_originsand supportedmodesexecution.url/visible/request_methodfieldsThose are all valid concerns for concrete step-up specs, especially 3DS, but they are intentionally deferred out of the base primitive. The tradeoff is explicit: this PR chooses the lower-risk “base envelope first” path. Standardized execution fields can still be introduced later once concrete actions prove the abstraction across multiple domains. Adding owner-defined
configfields later is non-breaking; prematurely standardizing base envelope fields would be much harder to unwind.Known concern / likely follow-up
The main concern we may continue to push on is that opaque
configmeans a generic platform cannot execute every action with one universal renderer. This PR does not claim to fully solve generic step-up execution. It establishes the shared action envelope, checkout lifecycle integration, action identity, and owner obligations. A stacked 3DS/action-specific PR should continue to iterate on the concrete execution, trust, negotiation, lifecycle, and recovery contract needed. For now, all of those validations, including trust, is expected to be uniquely defined by a given action.Category (Required)
Please select one or more categories that apply to this change.
Related Issues
#517
#458
Checklist
!for breaking changes).