Feature 1: Source Operation Binding
← Home | Next: Six Structural Constructs →
Most executable operations in UWS bind to an operation in a source document by reference. UWS 1.6 supports openapi, google-discovery, aws-smithy, asyncapi, graphql, openrpc, grpc-protobuf, odata, browser-profile, and ansible-module source descriptions directly. Extension-owned operations are the explicit non-source escape hatch. UWS never duplicates HTTP methods, paths, AsyncAPI channels, messages, GraphQL schemas, JSON-RPC method metadata, protobuf descriptors, OData metadata, browser profile locator/action/output details, Ansible module argspec details, schemas, servers, security schemes, or protocol metadata; those live in the source document, the separate versions/browser.1.5.{json,md} / versions/ansible.1.0.{json,md} sub-specs, or executor-owned configuration.
Three Mutually Exclusive Shapes
Every valid UWS operation matches exactly one of three shapes:
| Shape | sourceDescription |
Generic selector | Legacy OpenAPI selector | x-uws-operation-profile |
|---|---|---|---|---|
Source-bound by sourceOperationId or sourceOperationRef |
REQUIRED | Exactly one REQUIRED | MUST NOT be set | OPTIONAL |
OpenAPI-compatible by openapiOperationId or openapiOperationRef |
REQUIRED to an openapi source |
MUST NOT be set | Exactly one REQUIRED | OPTIONAL |
| Extension-owned | MUST NOT be set | MUST NOT be set | MUST NOT be set | REQUIRED |
A document that omits the binding fields of every shape is invalid. Source-bound operations may carry profile metadata, but an extension-owned operation is selected only when all source binding fields are absent.
Source Descriptions
sourceDescriptions[] declares every API or event source document the workflow uses. Every source entry must have a unique name matching ^[A-Za-z0-9_-]+$, a url, and optionally type.
Missing type defaults to openapi. UWS 1.6 added direct declarations for Ansible collection argspecs via type: ansible-module (with the heavier argspec profile schema published separately as versions/ansible.1.0.{json,md}). UWS 1.5 added browser capability profiles via type: browser-profile (with versions/browser.1.5.{json,md}). UWS 1.4 added direct declarations for Google Discovery, AWS Smithy, AsyncAPI, GraphQL, OpenRPC, gRPC/protobuf, and OData via type: google-discovery, type: aws-smithy, type: asyncapi, type: graphql, type: openrpc, type: grpc-protobuf, and type: odata. UWS 1.3 documents remain limited to OpenAPI, Google Discovery, AWS Smithy, and AsyncAPI sources. UWS 1.2 documents remain limited to OpenAPI, Google Discovery, and AWS Smithy sources. UWS 1.1 documents remain OpenAPI-bound: Discovery, Smithy, Stone, Postman Collection, RAML, API Blueprint, and similar non-OpenAPI artifacts can participate only after tooling lowers or converts them into reviewed OpenAPI, or as advisory evidence outside sourceDescriptions.
sourceDescriptions:
- name: petstore_api
url: ./petstore.yaml
type: openapi
- name: gmail_api
url: ./google-discovery/gmail.json
type: google-discovery
- name: lambda_api
url: ./aws-smithy/lambda.json
type: aws-smithy
- name: billing_events
url: ./asyncapi/billing-events.yaml
type: asyncapi
- name: issue_tracker
url: ./graphql/linear.graphql
type: graphql
- name: pet_rpc
url: ./openrpc/pets.json
type: openrpc
- name: inventory_service
url: ./protobuf/inventory.proto
type: grpc-protobuf
- name: successfactors
url: ./odata/successfactors.xml
type: odata
- name: builtin
url: ./ansible/ansible-builtin.argspec.json
type: ansible-module
sourceDescriptions is REQUIRED whenever any operation declares sourceDescription. A document where every operation is extension-owned may omit it.
Binding by operationId
The most common form: name the source description and the source operation identifier. UWS resolves the full operation from the source document.
{
"operationId": "list_pets",
"sourceDescription": "petstore_api",
"sourceOperationId": "listPets",
"request": {
"query": { "limit": 10 }
},
"outputs": {
"firstPet": "$response.body#/0"
}
}
Note what is absent: no HTTP method (GET), no path (/pets), no response schema. Those live in petstore.yaml. UWS only says which operation to call and how to use its result.
Two operations against two different APIs in one document:
sourceDescriptions:
- name: weather_api
url: ./weather.openapi.yaml
type: openapi
- name: gmail_api
url: ./gmail.openapi.yaml
type: openapi
operations:
- operationId: get_weather
sourceDescription: weather_api
sourceOperationId: getCurrentWeather
request:
query:
q: Los Angeles
outputs:
summary: $response.body.summary
- operationId: send_report
sourceDescription: gmail_api
sourceOperationId: gmail_users_messages_send
dependsOn: [get_weather]
request:
body:
userId: me
text: $steps.get_weather.outputs.summary
workflows:
- workflowId: main
type: sequence
steps:
- stepId: get_weather
operationRef: get_weather
- stepId: send_report
operationRef: send_report
Each operation points at a different source. UWS orchestrates them; neither OpenAPI document needs to know about the other.
Binding by JSON Pointer
When a source document does not assign a stable operation identifier, use a JSON Pointer fragment (sourceOperationRef) resolved against the named source:
{
"operationId": "get_pet_by_id",
"sourceDescription": "petstore_api",
"sourceOperationRef": "#/paths/~1pets~1{petId}/get"
}
The pointer MUST begin with #/. Slashes in path segments are escaped as ~1 per RFC 6901. sourceOperationRef and sourceOperationId MUST NOT be set together on the same operation. For OpenAPI sources only, legacy openapiOperationId and openapiOperationRef remain valid backward-compatible selectors.
For AsyncAPI sources, sourceOperationId names a root AsyncAPI Operation Object. UWS core validates sourceOperationRef as a JSON Pointer fragment; source-aware tooling and runtimes validate whether it resolves to a supported AsyncAPI target. sourceOperationRef should point to #/operations/<name> when possible; #/channels/<name> and #/channels/<name>/messages/<name> are compatibility targets for runtimes that can resolve them unambiguously.
For GraphQL, OpenRPC, gRPC/protobuf, and OData sources, UWS core validates only the source type, selector exclusivity, and JSON Pointer fragment shape. Source-aware tooling and runtimes validate whether a selector resolves to a supported GraphQL operation, JSON-RPC method, protobuf service method, OData entity operation, OData function, or OData action.
For Ansible module sources, sourceOperationId is the module's fully qualified collection name, such as ansible.builtin.apt. sourceOperationRef should point to #/modules/<fqcn> when a pointer form is preferred. Module arguments bind under request.body; source-aware tooling validates those values against the referenced versions/ansible.1.0 argspec document.
Extension-Owned Operations
Operations without a source binding are owned by a named runtime profile. x-uws-operation-profile names the profile; additional x-* fields carry profile-specific configuration.
operationId: build_email
x-uws-operation-profile: uws.runtime.1.0
x-uws-runtime:
type: fnct
function: mail_raw
arguments:
- from: bot@example.com
to: user@example.com
subject: Daily weather report
body: $steps.get_weather.outputs.summary
The validator accepts this as intentionally runtime-owned. See Extension Profiles for more.
Request Binding
The request object maps values into the referenced source operation's parameters and body. Keys map to common request locations:
request:
path:
petId: $steps.list.outputs.firstId
query:
includeDetails: true
format: json
header:
X-Request-Id: trace-abc-123
Accept-Language: en-US
cookie:
session: $variables.session_token
body:
status: available
tags:
- featured
Only path, query, header, cookie, body, and ^x- extension keys are permitted at the top level of request. Any other key is rejected:
For AsyncAPI source operations, use request.body for message payload or operation input values and request.header for AsyncAPI Message Object headers values when the source operation defines them. Channel parameter serialization and protocol binding behavior remain owned by the AsyncAPI document and runtime.
For GraphQL, OpenRPC, gRPC/protobuf, and OData source operations, request binding remains UWS-owned but interpretation of variable names, RPC params, protobuf fields, OData query options, and protocol-specific serialization remains source-aware and runtime-owned.
# This fails validation:
request:
params: # ← invalid key — use "query" or "path"
limit: 10
Outputs
outputs maps friendly names to runtime expressions evaluated after the operation runs:
operationId: search_products
sourceDescription: catalog_api
openapiOperationId: searchProducts
request:
query:
q: $variables.search_term
outputs:
total: $response.body.total
firstId: $response.body#/items/0/id
status: $response.statusCode
location: $response.headers.Location
These outputs are available to downstream steps as $steps.<stepId>.outputs.<name>.
What the Validator Rejects
The following are all invalid — each produces a structured error:
# Shape 1: both binding selectors set at once
- operationId: bad_op
sourceDescription: api
openapiOperationId: getOp
openapiOperationRef: "#/paths/~1foo/get"
# error: cannot specify both openapiOperationId and openapiOperationRef
# Shape 2: OpenAPI sourceDescription set but no selector
- operationId: bad_op
sourceDescription: api
# error: requires exactly one OpenAPI selector or generic source selector
# Shape 3: no binding at all, no profile
- operationId: bad_op
# error: requires a source binding or x-uws-operation-profile
# Shape 4: sourceDescription set but no operation selector
- operationId: also_bad
sourceDescription: api
x-uws-operation-profile: udon # profile metadata does not replace the missing selector
# error: requires exactly one source operation selector or OpenAPI selector
From The Big Fixture
The large round-trip fixture includes OpenAPI-bound compatibility operations with request locations, outputs, execution controls, criteria, and actions:
operation "fetch_ticket" {
sourceDescription = "incident_api"
openapiOperationId = "getIncident"
when = "$inputs.incidentId != ''"
forEach = "$variables.regions"
parallelGroup = "api_fetch_group"
outputs = {
severity = "$response.body.severity"
ticket = "$response.body"
}
request {
path {
incidentId = "$inputs.incidentId"
tenantId = "$inputs.tenantId"
}
query {
depth = "full"
include = ["timeline", "assets"]
}
}
timeout = 20
}
Full context: testdata/big/big.hcl.