Skip to content

Feature 2: Six Structural Constructs

← API Source Operation Binding | Next: Runtime Expression Grammar →


Operations are the leaves. Workflows and steps compose them using six structural control-flow constructs. Each workflow declares exactly one type; nested steps may also declare a structural type to form composite control flow.

This guide describes current UWS 1.12 behavior unless it identifies an older version explicitly. Version-gated behavior is selected by the document's declared UWS version.

sequence

Steps execute in declaration order. Each step completes before the next begins. Use sequence for any pipeline where outputs flow from one step to the next.

workflowId: checkout
type: sequence
steps:
  - stepId: validate_cart
    operationRef: validate_order
  - stepId: charge_card
    operationRef: charge_payment
    dependsOn: [validate_cart]
  - stepId: send_receipt
    operationRef: send_email
    dependsOn: [charge_card]

dependsOn within a sequence adds explicit cross-step dependencies beyond declaration order. items MUST NOT be set.

Adding a cross-workflow dependency from another workflow:

workflowId: post_checkout
type: sequence
dependsOn: [checkout]        # waits for the entire checkout workflow first
steps:
  - stepId: update_inventory
    operationRef: decrement_stock

parallel

Steps execute concurrently, subject only to dependsOn relationships. Use parallel when independent calls can overlap.

workflowId: enrichment
type: parallel
steps:
  - stepId: fetch_weather
    operationRef: get_weather
  - stepId: fetch_stocks
    operationRef: get_stocks
  - stepId: fetch_news
    operationRef: get_headlines

All three operations fire simultaneously. The construct completes when every step terminates.

parallelGroup as a dependency barrier:

workflowId: validate_and_merge
type: parallel
steps:
  - stepId: check_format
    operationRef: validate_format
    parallelGroup: validators     # member of the "validators" group

  - stepId: check_range
    operationRef: validate_range
    parallelGroup: validators     # also a member

  - stepId: aggregate
    operationRef: merge_results
    dependsOn: [validators]       # waits for ALL members of "validators"

dependsOn: [validators] waits for every step in the validators group. Membership in a group does not create additional ordering among members themselves — they still run concurrently. From UWS 1.10, a branch failure cancels sibling branch contexts. A goto or end control signal from a branch fails the parallel construct because cross-branch control flow is undefined.

switch

Cases are evaluated in declaration order. The first truthy when runs; a case without when is an unconditional match at its position. If no case matches, default runs if present. No later case runs.

workflowId: route_event
type: switch
cases:
  - name: new_user
    when: $trigger.body.event == "signup"
    steps:
      - stepId: welcome
        operationRef: send_welcome_email

  - name: returning_user
    when: $trigger.body.event == "login"
    steps:
      - stepId: log_access
        operationRef: record_login

default:
  - stepId: fallback_log
    operationRef: log_unknown_event

items MUST NOT be set on a switch.

switch inside a sequence step:

workflowId: process_order
type: sequence
steps:
  - stepId: fetch_order
    operationRef: get_order
  - stepId: route
    type: switch
    cases:
      - name: express
        when: $steps.fetch_order.outputs.tier == "express"
        steps:
          - stepId: expedite
            operationRef: fast_ship
      - name: standard
        when: $steps.fetch_order.outputs.tier == "standard"
        steps:
          - stepId: queue
            operationRef: standard_ship

loop

Iterates over an ordered JSON array. items is REQUIRED and must resolve to a JSON array at runtime. Each element is bound into the iteration scope as $item, with a zero-based $index; nested steps execute sequentially in source order.

workflowId: notify_all
type: loop
items: $outputs.subscribers
steps:
  - stepId: send_one
    operationRef: send_notification

loop with batchSize — processing in fixed groups:

workflowId: bulk_import
type: loop
items: $outputs.records           # e.g. array of 1000 records
batchSize: "50"                   # UWS 1.11 numeric literal: 50 items per batch
steps:
  - stepId: import_batch
    operationRef: bulk_upsert

batchSize MUST resolve to a positive integer. In UWS 1.11 and later, a complete JSON-number literal such as "50" is allowed; the numeric-literal grammar is not available to earlier declarations. Batches and iterations execute sequentially; $batchIndex is the zero-based batch number and is available only inside a loop. A successful loop produces an ordered array of {index, batchIndex, item} records, including an empty array when there are no items. cases and default MUST NOT be set on a loop.

merge

Combines the outputs of multiple upstream constructs named by dependsOn into a single structural result. Use merge after a parallel to collect independent results.

workflowId: gather_data
type: parallel
steps:
  - stepId: prices
    operationRef: get_prices
    parallelGroup: fetchers
  - stepId: ratings
    operationRef: get_ratings
    parallelGroup: fetchers
  - stepId: combine
    type: merge
    dependsOn: [fetchers]
    outputs:
      prices:  $steps.prices.outputs.data
      ratings: $steps.ratings.outputs.data

The corresponding result declaration:

results:
  - name: market_data
    kind: merge
    from: gather_data.combine
    value: $steps.combine.outputs

dependsOn is REQUIRED and MUST name at least one construct. items MUST NOT be set.

await

Evaluates its wait predicate immediately, then polls it at the executor's configured interval (200 ms by default). Nested steps run once after the predicate becomes truthy. An await does not re-execute already-completed steps or operations, so it cannot portably poll a status endpoint by placing that endpoint in a preceding sequence step.

workflowId: wait_for_external_signal
type: await
wait: $inputs.signal_ready == true
timeout: 300

The example requires a bound runtime or profile to supply an input whose value can change between predicate evaluations; UWS core does not define how an external signal is refreshed. Polling a remote job status requires an implementation-specific runtime/profile or separately scheduled invocations. timeout has been available on operations, workflows, and steps since UWS 1.1. A serialized timeout bounds the await; an executor-owned timeout MAY apply when it is absent. Context cancellation also stops polling. For non-await constructs, UWS 1.10 defines wait as a cancellable delay expression resolving to a finite number of seconds from 0 to 86,400, evaluated once before the body. In UWS 1.11 and later, a complete JSON-number literal may be used for this delay. cases, default, and items MUST NOT be set on await. See the UWS 1.12 execution contract.

Field Constraints Summary

Type items wait cases/default dependsOn
sequence MUST NOT optional MUST NOT optional
parallel MUST NOT optional MUST NOT optional
switch MUST NOT optional allowed optional
loop REQUIRED optional MUST NOT optional
merge MUST NOT optional MUST NOT REQUIRED (≥1)
await MUST NOT REQUIRED MUST NOT optional

The validator enforces every constraint above before the runtime sees the document.

Composing Types: Nested Steps

A top-level workflow always declares a type. A nested step may declare a type to become an inline structural construct, use operationRef to call an operation, or use workflow to invoke a top-level workflow by ID.

# A sequence whose third step is itself a loop
workflowId: process_batch
type: sequence
steps:
  - stepId: fetch_all
    operationRef: list_items
    outputs:
      items: $response.body.items

  - stepId: validate_all
    operationRef: validate_batch

  - stepId: process_each
    type: loop
    items: $steps.fetch_all.outputs.items
    steps:
      - stepId: handle_item
        operationRef: process_single_item

From The Big Fixture

The large fixture exercises structural workflows and nested workflow calls. This excerpt shows a sequence workflow step invoking another workflow:

workflow "main" {
  type        = "sequence"
  description = "Coordinate enrichment, runtime checks, branching, containment, and notification."
  dependsOn   = ["fetch_ticket", "load_customer"]
  outputs = {
    decision = "$steps.step_decide_path.outputs.selectedPath"
    incident = "$steps.step_collect_context.outputs.incident"
  }

  step "step_collect_context" {
    operationRef = "fetch_ticket"
    dependsOn    = ["run_cmd_primary", "run_fnct_primary"]
    outputs = {
      audit  = "$response.body.auditId"
      result = "$response.body.result"
    }
  }

  step "step_parallel_checks" {
    workflow  = "wf_parallel"
    dependsOn = ["step_collect_context", "load_customer"]
  }
}

Full context: testdata/big/big.hcl.


← API Source Operation Binding | Next: Runtime Expression Grammar →