Skip to content

Feature 1: OpenAPI Operation Binding

Home | Next: Six Structural Constructs →


Most executable operations in UWS bind to an existing OpenAPI operation by reference. Extension-owned operations are the explicit non-OpenAPI escape hatch. UWS never duplicates the HTTP method, path, request schema, response schema, server, or security scheme — those live in the OpenAPI document.

Three Mutually Exclusive Shapes

Every valid UWS operation matches exactly one of three shapes:

Shape sourceDescription openapiOperationId openapiOperationRef x-uws-operation-profile
OpenAPI-bound by operationId REQUIRED REQUIRED MUST NOT be set OPTIONAL
OpenAPI-bound by JSON Pointer REQUIRED MUST NOT be set 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. OpenAPI-bound operations may carry profile metadata, but an extension-owned operation is selected only when all OpenAPI binding fields are absent.

Source Descriptions

sourceDescriptions[] declares every OpenAPI document the workflow uses. Every source entry must have a unique name matching ^[A-Za-z0-9_-]+$, a url, and optionally type: openapi.

Google Discovery and AWS Smithy inputs are not separate sourceDescriptions.type values in UWS 1.1. Compliant tooling may lower those source model families into OpenAPI-bound operations and preserve native source metadata in x-* extensions.

sourceDescriptions:
  - name: petstore_api
    url: ./petstore.yaml
    type: openapi
  - name: gmail_api
    url: https://raw.githubusercontent.com/example/gmail-openapi/main/openapi.yaml
    type: openapi
  - name: stripe_api
    url: ./stripe.openapi.json
    type: openapi

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 OpenAPI operationId. UWS resolves the full operation — method, path, server, security — from the OpenAPI document.

{
  "operationId": "list_pets",
  "sourceDescription": "petstore_api",
  "openapiOperationId": "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
    openapiOperationId: getCurrentWeather
    request:
      query:
        q: Los Angeles
    outputs:
      summary: $response.body.summary

  - operationId: send_report
    sourceDescription: gmail_api
    openapiOperationId: sendMessage
    dependsOn: [get_weather]
    request:
      body:
        userId: me
        text: $steps.fetch.outputs.summary

Each operation points at a different source. UWS orchestrates them; neither OpenAPI document needs to know about the other.

Binding by JSON Pointer

When an OpenAPI document does not assign a stable operationId, use a JSON Pointer fragment (openapiOperationRef) resolved against the named source:

{
  "operationId": "get_pet_by_id",
  "sourceDescription": "petstore_api",
  "openapiOperationRef": "#/paths/~1pets~1{petId}/get"
}

The pointer MUST begin with #/. Slashes in path segments are escaped as ~1 per RFC 6901. openapiOperationRef and openapiOperationId MUST NOT be set together on the same operation.

Extension-Owned Operations

Operations without an OpenAPI 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 OpenAPI operation's parameters and body. Keys map to OpenAPI parameter 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:

# 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: sourceDescription set but no selector
- operationId: bad_op
  sourceDescription: api
  # error: requires exactly one of openapiOperationId or openapiOperationRef

# Shape 3: no binding at all, no profile
- operationId: bad_op
  # error: requires an OpenAPI binding or x-uws-operation-profile

# Shape 4: sourceDescription set but no OpenAPI selector
- operationId: also_bad
  sourceDescription: api
  x-uws-operation-profile: udon   # profile metadata does not replace the missing selector
  # error: requires exactly one of openapiOperationId or openapiOperationRef

From The Big Fixture

The large round-trip fixture includes OpenAPI-bound 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.


Home | Next: Six Structural Constructs →