IPA-132: Long-Running Operations
A long-running operation (LRO) is an operation whose completion is not guaranteed within a single HTTP request/response cycle. Rather than holding a request open until the work finishes, the API accepts the request, returns a handle to the work immediately, and lets the client observe progress asynchronously by polling a dedicated Operation resource.
Classification is structural, decided at design time: an operation is long-running because of what it does (it hands work to a background worker, or its worst-case time scales with an uncapped input), not because a particular request happened to be slow. Slowness that can be fixed (caching, an index, query optimization) is a performance bug, not an LRO.
Guidance
Classifying an operation as long-running
An operation must be classified as long-running at design phase when its completion is not guaranteed within a single request/response cycle, or when it is executed asynchronously (for example via a planner, job queue, or workflow engine).
paths:
/orders:
post:
operationId: createOrder
responses:
"202":
description: Accepted
headers:
Location:
schema:
type: string
format: uriWhy:Creating an order hands work to a background worker, so completion is not guaranteed in-cycle; the method is correctly modeled as an LRO and must only return 202 Accepted and a Location header.
paths:
/users:
post:
operationId: createUser
responses:
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/User"Why:Creating a user is performed as a single insert that is expected to complete within the request, so the client does not need any operation handle to poll and the method should not be modeled as an LRO.
For each mutating operation (POST, PUT, PATCH, DELETE, or a custom method), determine whether the handler can complete the work on the request thread or must hand it to a background worker (planner, job queue, workflow engine).
Determine whether the worst-case completion time scales with an input that is not capped (fan-out, snapshot size, queue depth, an external system). An uncapped input is a strong LRO signal.
If either signal holds, the operation is long-running and must follow this principle. If the operation is merely slow but bounded and fixable, treat it as a performance issue, not an LRO.
Confirm a long-running operation can only return 202 Accepted.
A Get method must not be an LRO.
- Get method returns the current state of a single resource that already resides on the server.
paths:
/orders/{orderId}:
get:
operationId: getOrder
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Order"Why:Fetching an order by ID reads state that already exists on the server and returns it synchronously with 200 OK, so there is no background work to track and the method is correctly not modeled as an LRO.
paths:
/orders/{orderId}:
get:
operationId: getOrder
responses:
"202":
description: Accepted
headers:
Location:
description: URI of the Operation resource to poll.
schema:
type: string
format: uriWhy:Fetching an order by ID reads state that already exists on the server, so modeling the Get method as a long-running operation adds an Operation handle where no background work exists to track.
A List method must not be an LRO.
- List method returns data from a collection that already exists on the server.
paths:
/orders:
get:
operationId: listOrders
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
results:
type: array
items:
$ref: "#/components/schemas/Order"Why:Listing orders returns a finite page of records that already exist on the server, so the response is returned in-cycle with 200 OK and does not need an operation handle.
paths:
/orders/{orderId}:
get:
operationId: listOrders
responses:
"202":
description: Accepted
headers:
Location:
description: URI of the Operation resource to poll.
schema:
type: string
format: uriWhy:Listing orders returns a page of existing collection data, so modeling the List method as a long-running operation incorrectly turns a synchronous read into asynchronous work.
Initiating a long-running operation
A method that starts long-running work acknowledges the request immediately and returns a handle the client uses to observe it.
A method that starts a long-running operation must return 202 Accepted with a Location header pointing at the Operation resource URI that clients poll for status.
paths:
/orders:
post:
operationId: createOrder
responses:
"202":
description: Accepted
headers:
Location:
description: URI of the Operation resource to poll.
schema:
type: string
format: uriWhy:Creating an order starts background work, so the method acknowledges the request with 202 Accepted and hands the client the exact Operation URI to poll in the Location header, providing one unambiguous way to obtain the handle.
paths:
/orders:
post:
operationId: createOrder
responses:
"202":
description: AcceptedWhy:Creating an order starts long-running work, but the method returns 202 Accepted without a Location header, so the client has no Operation resource URI to poll and no standard way to observe progress.
A method modeled as a long-running operation must not advertise any other 2XX success status code besides 202 Accepted.
paths:
/orders:
post:
operationId: createOrder
responses:
"202":
description: Accepted
headers:
Location:
description: URI of the Operation resource to poll.
schema:
type: string
format: uriWhy:Because the method is correctly marked as an LRO and limits its success response to 202 Accepted, the contract for starting work remains definitive and declarative tooling can consistently depend on this uniform behavior.
paths:
/orders:
post:
operationId: createOrder
responses:
"202":
description: Accepted
headers:
Location:
schema:
type: string
format: uri
"201":
description: Created
content:
application/json:
schema:
$ref: "#/components/schemas/Order"Why:The same long-running operation advertises two success codes, so clients cannot reliably tell whether they should expect a body or a handle and automated validation cannot enforce a single contract.
For each Long-Running operation, collect all 2XX responses.
- Confirm the only 2XX response is 202 Accepted.
Flag any long-running operation that advertises 200, 201, 204, or any other 2XX code apart from 202.
The 202 Accepted response must not contain content.
- The handle to the operation is carried by the Location header, not the body.
responses:
"202":
description: Accepted
headers:
Location:
schema:
type: string
format: uriWhy:The 202 Accepted response carries only the Location header and no body, so there is a single, unambiguous way for the client to obtain the operation handle.
responses:
"202":
description: Accepted.
content:
application/json:
schema:
$ref: "#/components/schemas/Order"Why:The 202 Accepted response returns an Order body alongside the handle, implying the resource already exists and creating a second, conflicting source of truth alongside the Operation resource.
The API must perform all applicable request validation and authorization before accepting the request for long-running work, so invalid or unauthorized requests still receive a synchronous 4xx and never a 202.
paths:
/orders:
post:
operationId: createOrder
responses:
"202":
description: Accepted
"400":
description: Bad Request
"401":
description: Unauthorized
"403":
description: ForbiddenWhy:The order create validates the payload and caller permissions on the request thread and only then enqueues the background work, so invalid or unauthorized requests receive synchronous 4xx responses and 202 Accepted is reserved for real work.
paths:
/orders:
post:
operationId: createOrder
responses:
"202":
description: AcceptedWhy:The order create accepts every request unconditionally and leaves the background worker to discover invalid input, so clients must poll an Operation only to learn the request was malformed, instead of getting a clear synchronous 4xx.
For each long-running method, confirm the operation declares the synchronous error responses that apply (400, 401, 403, 409, 422, …).
Confirm the description or design makes clear that validation and authorization run before the request is accepted for asynchronous work.
Flag any endpoint that returns 202 unconditionally and reports validation or authorization failures only through the Operation resource.
The Operation resource
A long-running operation is observed through a dedicated, read-only Operation resource exposed on its parent.
An operation marked as long-running must expose its status through an
/operationsendpoint on its parent resource.- If the long-running operation is initiated from a collection-level method and the resource instance does not yet exist, the Operation resource must be exposed at the collection level.
- If the long-running operation is initiated for an existing resource instance, the Operation resource must be exposed at the instance level.
paths:
/orders:
post:
operationId: createOrder
responses:
"202":
description: Accepted
headers:
Location:
schema:
type: string
format: uri
/orders/operations:
get:
operationId: listOrdersOperations
/orders/operations/{operationId}:
get:
operationId: getOrdersOperationWhy:A collection-level create that is long-running exposes its Operation resources under
/orders/operationsand/orders/operations/{operationId}, so the handle in the Location header always points at a concrete Operations endpoint in the contract.paths:
/orders/{orderId}:
patch:
operationId: updateOrder
responses:
"202":
description: Accepted
headers:
Location:
schema:
type: string
format: uri
/orders/{orderId}/operations:
get:
operationId: listOrderOperations
/orders/{orderId}/operations/{operationId}:
get:
operationId: getOrderOperationWhy:A long-running update on an existing order instance exposes its Operation resources under
/orders/{orderId}/operationsand its instance-level counterpart/orders/{orderId}/operations/{operationId}, making the parent–child relationship explicit.paths:
/orders:
post:
operationId: createOrder
responses:
"202":
description: Accepted
headers:
Location:
schema:
type: string
format: uriWhy:The createOrder method is marked long-running and returns a Location header, but no /orders/operations endpoints are defined, so the handle points at a resource that does not exist in the contract.
- Derive the parent resource path for each operation.
If the long-running operation is initiated at collection level and no resource instance exists yet, confirm the paths object defines
<parent>/operationsand<parent>/operations/{operationId}.If the long-running operation is initiated on an existing resource instance, confirm the paths object defines
<parent>/{resourceId}/operationsand<parent>/{resourceId}/operations/{operationId}.Flag any resource that tracks instance-level long-running operations only at collection scope, either by pointing the Location header at a collection-scoped Operation or by omitting the instance-scoped Operations endpoints.
Operations resource must be a read-only resource.
paths:
/orders/{orderId}/operations:
get:
operationId: listOrderOperations
/orders/{orderId}/operations/{operationId}:
get:
operationId: getOrderOperationWhy:The Operations endpoints expose only GET methods under
/orders/{orderId} /operationsand/orders/{orderId}/operations/{operationId}, matching the read-only, backend-managed lifecycle of Operation records.Operations endpoints must be defined as nested paths under the parent resource's own path, at both the collection and instance level.
noteCompatibility note with IPA-102: The
/operationsand/operations/{operationId}segments are a standardized LRO suffix.paths:
/groups/{groupId}/orders/operations:
get:
operationId: listOrdersOperations
/groups/{groupId}/orders/{orderId}/operations:
get:
operationId: listOrderOperationsWhy:Order operations are exposed under
/groups/{groupId}/orders/operationsand/groups/{groupId}/orders/{orderId}/operations, so they remain nested under the exact same path as their parent resource.paths:
/groups/{groupId}/orders/{orderId}:
patch:
operationId: updateOrder
responses:
"202":
description: Accepted
headers:
Location:
schema:
type: string
format: uri
/orgs/{orgId}/orders/{orderId}/operations:
get:
operationId: listOrderOperations
/orgs/{orgId}/orders/{orderId}/operations/{operationId}:
get:
operationId: getOrderOperationWhy:These Operations endpoints sit at a path detached from the parent order, namely
/orgs/{orgId}/orders/{orderId}/operations, instead of nested under the order's own path at/groups/{groupId}/orders/{orderId}, so the Operations endpoint no longer stems from its parent resource's path.Identify the parent resource's own path for each long-running operation.
Confirm the corresponding Operations endpoints are defined as nested paths directly under that parent resource path, at both the collection and instance level.
Flag any Operations endpoint whose path does not stem from its parent resource's own path.
Operations endpoints must not be defined as standalone, global endpoints with no parent resource in their path.
paths:
/groups/{groupId}/orders/operations:
get:
operationId: listOrdersOperations
/groups/{groupId}/orders/operations/{operationId}:
get:
operationId: getOrdersOperationWhy:The Operations endpoints are nested under the
/groups/{groupId}/orderscollection path, so they are not exposed as standalone, parentless endpoints.paths:
/groups/{groupId}/orders/operations:
get:
operationId: listOrdersOperations
/orders/operations:
get:
operationId: listAllOrderOperationsWhy:A single global /orders/operations endpoint has no parent resource in its path, so it is detached from the resource hierarchy the operation belongs to.
Collect every path ending in
/operationsor/operations/{operationId}.Flag any such path mounted at the API root or with no parent resource segment preceding it.
Confirm each Operations endpoint's path is nested under a parent resource path.
Operations endpoints must be leaf resources. An
operationssegment may only be followed by a single operation identifier path parameter.paths:
/groups/{groupId}/orders/operations:
get:
operationId: listOrdersOperations
/groups/{groupId}/orders/operations/{operationId}:
get:
operationId: getOrdersOperationWhy:The Operations endpoints end at the
operationscollection and at the single operation identified by{operationId}, so no resources are nested below the Operations resource.paths:
/groups/{groupId}/orders/operations/{operationId}/logs:
get:
operationId: listOrdersOperationLogsWhy:The
logsresource is nested below a single Operation, so the Operations endpoint is no longer a leaf resource.Collect every path containing an
operationssegment.Confirm each such path ends at the
operationssegment or at a single operation identifier path parameter directly following it.Flag any path that nests further segments below the
operationssegment or below its operation identifier path parameter.
Operations endpoints must use the same authorization model as read access on the parent resource. They must not introduce broader access, stronger permissions, or a separate permission model.
paths:
/groups/{groupId}/orders/{orderId}:
get:
operationId: getOrder
x-rolesRequirements:
- Project Read Only
/groups/{groupId}/orders/{orderId}/operations/{operationId}:
get:
operationId: getOrderOperation
x-rolesRequirements:
- Project Read OnlyWhy:Reading an order operation requires the same role set as reading the parent order, so the Operations endpoint does not introduce a separate authorization surface.
paths:
/groups/{groupId}/orders/{orderId}:
get:
operationId: getOrder
x-rolesRequirements:
- Project Read Only
/groups/{groupId}/orders/{orderId}/operations/{operationId}:
get:
operationId: getOrderOperation
x-rolesRequirements:
- Project OwnerWhy:Reading the Operation requires a stronger role than reading the parent resource, so the Operations endpoint introduces a different authorization model from the parent read.
Communicating the Long Running Operation Status
Operation endpoints must return
OperationResponseto report long-running operation status.paths:
/orders/{orderId}/operations/{operationId}:
get:
operationId: getOrderOperation
responses:
"200":
description: Operation status.
content:
application/json:
schema:
$ref: "#/components/schemas/OperationResponse"Why:The Operation endpoint returns the standard OperationResponse schema, so clients have one consistent contract for reading long-running operation state.
paths:
/orders/{orderId}/operations/{operationId}:
get:
operationId: getOrderOperation
responses:
"200":
description: Operation status.
content:
application/json:
schema:
$ref: "#/components/schemas/Order"Why:The Operation endpoint returns an Order resource instead of OperationResponse, so the long-running operation status contract is not represented through the standard Operation schema.
An Operation must report progress through a
statusfield, using exactly this enum:PENDING— the request was accepted but work has not started.IN_PROGRESS— work is currently executing.SUCCEEDED— work completed successfully. This is the only success terminal state.FAILED— work completed unsuccessfully and reports a structurederror.CANCELED— work was terminated on request before completion.SUPERSEDED— work was replaced by a later operation on the same resource.
components:
schemas:
OperationResponse:
type: object
properties:
status:
type: string
enum: [PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED]Why:The OperationResponse schema reports its lifecycle through a single status field whose enum is exactly [PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED], so a generic client can reason about any operation the same way and SUCCEEDED is the single success terminal.
components:
schemas:
OperationResponse:
type: object
properties:
state:
type: string
enum: [QUEUED, RUNNING, COMPLETE, ERROR, CANCELLED]Why:The schema renames the field to state and invents a different enum, so a generic client cannot map these values to the standard lifecycle and even the spelling CANCELLED diverges from the canonical set.
A FAILED operation must report the failure through a structured
errorobject carrying acode, amessage, aretryableflag, an optionalretryStrategy, and optionaldetails.components:
schemas:
OperationResponse:
type: object
properties:
error:
$ref: "#/components/schemas/OperationError"
OperationError:
type: object
properties:
code:
type: string
message:
type: string
retryable:
type: boolean
retryStrategy:
type: string
enum: [IMMEDIATE, BACKOFF, NONE]
details:
type: objectWhy:A failed order create returns a structured OperationError with code, message, retryable, plus optional retryStrategy, and optional details, so a client can decide whether to retry, back off, or escalate without parsing free text.
components:
schemas:
OperationResponse:
type: object
properties:
error:
type: stringWhy:A failed order create represents its failure as a plain error string, forcing every client to parse prose and giving no machine-readable signal of whether the failure is retryable.
Find the Operation schema and its error property.
Confirm error is an object with code, message, retryable, with retryStrategy and details optional.
Flag operations that model failure as a plain string or omit the retryable signal.
On SUCCEEDED, an Operation must expose a
resultHrefpointing at the completed resource, so a client can follow the operation straight to its result.components:
schemas:
OperationResponse:
type: object
properties:
status:
type: string
enum: [PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED]
resultHref:
type: string
format: uriWhy:A succeeded order create sets resultHref to the finished order's URI, so the client can follow the Operation directly to the completed resource instead of reconstructing the URL.
components:
schemas:
OperationResponse:
type: object
properties:
status:
type: string
enum: [PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED]Why:The succeeded order create exposes only a status field and no resultHref, so the client must guess where the completed resource lives.
An Operation should expose
statusMessage,progress, andestimatedCompletionTimeso clients can reason about in-flight work.components:
schemas:
OperationResponse:
type: object
properties:
statusMessage:
type: string
progress:
type: object
properties:
completed:
type: number
total:
type: number
unit:
type: string
estimatedCompletionTime:
type: string
format: date-timeWhy:An in-flight order operation provides a human-readable statusMessage and quantitative progress so the client can see how far along the work is.
components:
schemas:
OperationResponse:
type: object
properties:
status:
type: string
enum: [PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED]Why:The in-flight order operation exposes only the status enum, giving the client no progress signal or poll-cadence hint and making it hard to choose sensible polling behaviour.
While an Operation is in a non-terminal state (
PENDINGorIN_PROGRESS), it must exposeretryAfterSeconds, the suggested minimum poll interval in seconds, so clients know how often to poll rather than guessing. Once an Operation reaches a terminal state,retryAfterSecondsmust be omitted, since there is no further work to poll for.components:
schemas:
OperationResponse:
type: object
properties:
status:
type: string
enum: [PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED]
retryAfterSeconds:
type: integerWhy:An in-flight order operation exposes retryAfterSeconds alongside status, so the client has a concrete poll-cadence hint instead of guessing an interval on its own; the field is omitted once the operation reaches a terminal state.
components:
schemas:
OperationResponse:
type: object
properties:
status:
type: string
enum: [PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED]Why:The in-flight order operation exposes only the bare status enum, giving the client no poll-cadence hint and risking either overly aggressive polling or needlessly long waits.
Operation records must be transient: they must expire after a finite retention period, exposed via an
expiresAttimestamp on the Operation schema indicating when this will happen. After expiry the Operations endpoint returns 404; the underlying resource read is unaffected.components:
schemas:
OperationResponse:
type: object
properties:
expiresAt:
type: string
format: date-time
description:
When this operation record expires and returns 404. Default
retention is ~30 days after reaching a terminal state.Why:The order operation record exposes an expiresAt timestamp that tells the client when the record will return 404, so operation history remains transient and storage does not grow without bound.
components:
schemas:
OperationResponse:
type: object
properties:
createdAt:
type: string
format: date-timeWhy:The order operation record has a createdAt time but no expiry, implying operation history is retained forever instead of expiring after a finite retention period.
Confirm the Operation schema exposes an expiry timestamp (for example expiresAt) and that the implementation enforces a finite TTL on terminal records.
Confirm expired operations return 404 from the Operations endpoint while the parent resource read is unaffected.
Flag operations retained indefinitely or with no expiry signal.
An OperationResponse must include stable core metadata for the operation record: a required
operationId, a requiredoperationTypeusing exactly the enumCREATE,UPDATE,DELETE,CUSTOM, acustomMethodthat is required whenoperationTypeisCUSTOMand must be omitted otherwise, a requiredcreatedAt, and a requiredupdatedAt.components:
schemas:
OperationResponse:
type: object
properties:
operationId:
type: string
operationType:
type: string
enum: [CREATE, UPDATE, DELETE, CUSTOM]
customMethod:
type: string
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-timeWhy:The OperationResponse exposes a stable identifier, a standard operation kind, and record timestamps, so generic clients and tooling can identify the operation, understand what kind of work it represents, and tell when its status last changed.
components:
schemas:
OperationResponse:
type: object
properties:
operationType:
type: string
enum: [CREATE, UPDATE, DELETE, CUSTOM]
createdAt:
type: string
format: date-timeWhy:Missing operationId and updatedAt means consumers cannot uniquely track the record or determine the freshness of the reported status.
Locate the schema defined for Operation results.
Verify that operationId, operationType, createdAt, and updatedAt are required fields.
Confirm the operationType enum matches exactly [CREATE, UPDATE, DELETE, CUSTOM].
Ensure customMethod is strictly tied to CUSTOM operations and omitted for others.
Reading a resource during a long-running operation
Reads on the resource itself (Get and List) must not be used to convey operation status. A resource read returns the resource's current stored state; operation status is the Operation endpoint's responsibility.
paths:
/orders/{orderId}:
get:
operationId: getOrder
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
/orders/{orderId}/operations/{operationId}:
get:
operationId: getOrderOperation
responses:
"200":
description: Operation status.
content:
application/json:
schema:
$ref: "#/components/schemas/OperationResponse"Why:The order read at
/orders/{orderId}returns only the stored Order state, while operation status is exposed separately at/orders/{orderId}/operations/ {operationId}, so resource representation and Operation lifecycle stay cleanly separated.paths:
/orders/{orderId}:
get:
operationId: getOrder
responses:
"200":
description: OK
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/Order"
- type: object
properties:
operationStatus:
type: string
enum: [PENDING, IN_PROGRESS, SUCCEEDED, FAILED]Why:The order read embeds an operationStatus field alongside the Order schema, coupling the resource representation to LRO lifecycle concerns and giving clients two competing sources of truth for progress.
Inspect the Get and List response schemas of resources that support long-running mutations.
Confirm they carry only resource state, not operation-lifecycle fields (operation status, progress, operation error).
Flag resource reads that surface operation status instead of leaving it to the Operation endpoint.
Depends onChanges to a resource initiated by a long-running operation must not be applied or persisted until the operation reaches a successful terminal state. If the operation fails, the resource representation must reflect its last successfully applied state.
GET /groups/{groupId}/orders/{orderId}
200 OK
{ "orderId": "order-1", "quantity": 10 }
PATCH /groups/{groupId}/orders/{orderId}
{ "quantity": 25 }
202 Accepted
Location: /groups/{groupId}/orders/{orderId}/operations/op-1
GET /groups/{groupId}/orders/{orderId}/operations/op-1
200 OK
{ "operationId": "op-1", "status": "IN_PROGRESS" }
GET /groups/{groupId}/orders/{orderId}
200 OK
{ "orderId": "order-1", "quantity": 10 }
GET /groups/{groupId}/orders/{orderId}/operations/op-1
200 OK
{ "operationId": "op-1", "status": "SUCCEEDED" }
GET /groups/{groupId}/orders/{orderId}
200 OK
{ "orderId": "order-1", "quantity": 25 }Why:While the operation is
IN_PROGRESSthe order still reads as10; the requested change only becomes visible once the operation reachesSUCCEEDED.GET /groups/{groupId}/orders/{orderId}
200 OK
{ "orderId": "order-1", "quantity": 10 }
PATCH /groups/{groupId}/orders/{orderId}
{ "quantity": 25 }
202 Accepted
Location: /groups/{groupId}/orders/{orderId}/operations/op-1
GET /groups/{groupId}/orders/{orderId}/operations/op-1
200 OK
{ "operationId": "op-1", "status": "FAILED", "error": { ... } }
GET /groups/{groupId}/orders/{orderId}
200 OK
{ "orderId": "order-1", "quantity": 10 }Why:The operation reached
FAILED, so the requested quantity change was never persisted and the order still reads as10, its last successfully applied state.GET /groups/{groupId}/orders/{orderId}
200 OK
{ "orderId": "order-1", "quantity": 10 }
PATCH /groups/{groupId}/orders/{orderId}
{ "quantity": 25 }
202 Accepted
Location: /groups/{groupId}/orders/{orderId}/operations/op-1
GET /groups/{groupId}/orders/{orderId}/operations/op-1
200 OK
{ "operationId": "op-1", "status": "FAILED", "error": { ... } }
GET /groups/{groupId}/orders/{orderId}
200 OK
{ "orderId": "order-1", "quantity": 25 }Why:The order reports the requested quantity of
25even though the operation failed, so the resource exposes a change that was never successfully applied and clients cannot tell which state is authoritative.For each long-running mutation, identify the point at which the implementation writes the requested changes to the resource's durable store.
Confirm that write happens only once the operation reaches a successful terminal state, and that a failed operation leaves the stored resource at its last successfully applied state.
Flag any long-running mutation that writes requested changes before the work succeeds, or that leaves partially applied changes readable after a failure.
OperationResponse schema
{
// Required. Unique identifier for this Operation.
"operationId": "string",
// Required. Enum: PENDING, IN_PROGRESS, SUCCEEDED, FAILED, CANCELED, SUPERSEDED.
"status": "PENDING",
// Required. Enum: CREATE, UPDATE, DELETE, CUSTOM.
"operationType": "CREATE",
// Required when operationType is CUSTOM; omitted otherwise.
"customMethod": "promoteCluster",
// Optional human-readable progress note. Not for parsing.
"statusMessage": "string",
// Optional quantitative progress.
"progress": {
// Optional completed units.
"completed": 42,
// Optional total units.
"total": 100,
// Optional unit label (for example "nodes", "shards", "percent").
"unit": "nodes"
},
// Present on FAILED only.
"error": {
// Required. Stable, programmatic error code.
"code": "string",
// Required. Human-readable summary of the failure.
"message": "string",
// Required. Whether the client may safely retry.
"retryable": true,
// Optional. Enum: IMMEDIATE, BACKOFF, NONE.
"retryStrategy": "BACKOFF",
// Optional. Structured metadata with provider-specific fields.
"details": {}
},
// Required on SUCCEEDED. URL of the completed resource.
"resultHref": "https://api.example.com/...",
// Required while non-terminal (PENDING or IN_PROGRESS). Must be omitted once the operation reaches a terminal state.
"retryAfterSeconds": 30,
// Optional non-binding completion estimate.
"estimatedCompletionTime": "2026-07-01T12:34:56Z",
// Required. Creation timestamp of the Operation record.
"createdAt": "2026-07-01T12:00:00Z",
// Required. Last status update timestamp.
"updatedAt": "2026-07-01T12:01:00Z",
// Required. When the Operation record expires and returns 404. Default retention ~30 days after terminal state.
"expiresAt": "2026-08-01T12:00:00Z"
}
Example
{
"operationId": "5f2e1a9c3b7d4e6f8a0b1c2d3e4f5a6b",
"status": "SUCCEEDED",
"operationType": "CREATE",
"statusMessage": "Order created successfully.",
"resultHref": "https://api.example.com/orders/64f1b2e3c4d5e6f7a8b9c0d1",
"createdAt": "2026-07-01T12:00:00Z",
"updatedAt": "2026-07-01T12:03:00Z",
"expiresAt": "2026-08-01T12:00:00Z"
}
Motivation and Strategic Goals
A single, enforceable LRO contract lets us auto-generate declarative tooling (Terraform, Kubernetes operators, SDKs) for asynchronous work instead of hand-writing endpoint-specific polling logic, structured status and errors also make long-running failures debuggable for customers, support, and agents rather than hidden behind loosely defined state fields.