Feature 2: Six Structural Constructs
← OpenAPI 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.
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.
switch
Exactly one case whose when evaluates truthy runs its steps. If no case matches, default runs if present; otherwise the construct emits no result.
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 a JSON array. items is REQUIRED and must resolve to a JSON array at runtime. Each element is bound into the iteration scope for use in nested steps.
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" # process 50 at a time
steps:
- stepId: import_batch
operationRef: bulk_upsert
batchSize MUST resolve to a positive integer. Batches execute sequentially; within each batch the items are available as the iteration context. 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
Blocks execution until its wait expression evaluates truthy. Use await to poll for an async job to complete, or to wait for an external signal.
workflowId: wait_for_job
type: await
wait: $outputs.job_status == "done"
await after kicking off an async job:
workflowId: run_report
type: sequence
steps:
- stepId: submit
operationRef: start_report_job
outputs:
job_id: $response.body.jobId
- stepId: check_status
operationRef: get_job_status
request:
path:
jobId: $steps.submit.outputs.job_id
- stepId: wait_until_complete
type: await
wait: $steps.check_status.outputs.status == "complete"
timeout: 300
- stepId: download
operationRef: fetch_report_result
timeout is a serialized UWS 1.1 field on operations, workflows, and steps. Runtime polling behavior remains executor-owned. cases, default, and items MUST NOT be set on await.
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.
← OpenAPI Operation Binding | Next: Runtime Expression Grammar →