Feature 5: Structural Results
← Triggers and Route Dispatch | Next: Success Criteria and Actions →
A structural result gives a named, addressable identity to the output of a structural construct — a workflow or step whose type is switch, merge, or loop. Without this declaration, a construct's output is anonymous control flow. With it, the output becomes a named artifact that downstream code can reference.
Structural Result Object Fields
| Field | Required | Description |
|---|---|---|
name |
REQUIRED | Unique result name within results[]. MUST match ^[a-zA-Z0-9._-]+$ |
kind |
REQUIRED | One of switch, merge, loop. MUST equal the type of the referenced construct |
from |
REQUIRED | <workflowId> or <workflowId>.<stepId> of the emitting construct |
value |
optional | Runtime expression selecting the value to expose; implementation-defined when omitted |
The from Field
from identifies the emitting construct in one of two forms:
<workflowId>— a top-level workflow whosetypeisswitch,merge, orloop.<workflowId>.<stepId>— a step within a named workflow whosetypeisswitch,merge, orloop.
The validator resolves from to a real workflow or step, then checks that the referenced type matches kind. A mismatch produces a structured error.
Why Only Three Kinds?
switch, merge, and loop each produce a meaningful aggregate result:
switch— the output of whichever branch ran (or nothing if no branch matched).merge— the combined outputs of multiple upstream constructs.loop— the accumulated results of iterating over an array.
sequence, parallel, and await do not produce a single named aggregate output — their outputs flow through step-level outputs maps instead.
Example 1: merge Result — Combining Parallel Checks
Two validation steps run in parallel; a merge step collects their results; a named result exposes the combined output.
workflows:
- workflowId: validate_order
type: parallel
steps:
- stepId: check_inventory
operationRef: validate_stock
parallelGroup: validators
outputs:
ok: $response.body.available
message: $response.body.reason
- stepId: check_credit
operationRef: validate_payment
parallelGroup: validators
outputs:
ok: $response.body.approved
limit: $response.body.creditLimit
- stepId: combine_checks
type: merge
dependsOn: [validators]
outputs:
inventory_ok: $steps.check_inventory.outputs.ok
credit_ok: $steps.check_credit.outputs.ok
credit_limit: $steps.check_credit.outputs.limit
results:
- name: order_validation
kind: merge
from: validate_order.combine_checks
value: $steps.combine_checks.outputs
order_validation is now a named, addressable result. Any downstream logic that needs to know "did the validation pass?" references this result by name.
Example 2: loop Result — Accumulated Iteration Output
A loop processes each item in an array and collects the results into a named output.
workflows:
- workflowId: import_records
type: loop
items: $outputs.pending_records
steps:
- stepId: upsert_record
operationRef: create_or_update
outputs:
record_id: $response.body.id
created: $response.body.created
results:
- name: import_summary
kind: loop
from: import_records
value: $steps.upsert_record.outputs
import_summary names the loop output. The runtime defines what "accumulated loop results" means in practice (e.g. an array of per-iteration outputs), but the named result is the UWS-level handle.
Example 3: switch Result — Named Branch Decision
A switch construct selects one processing path; the result names which branch ran and what it produced.
workflows:
- workflowId: classify_event
type: switch
cases:
- name: high_value
when: $trigger.amount >= 1000
steps:
- stepId: premium_process
operationRef: handle_premium_order
outputs:
tier: "premium"
discount: $response.body.appliedDiscount
- name: standard
when: $trigger.amount < 1000
steps:
- stepId: normal_process
operationRef: handle_standard_order
outputs:
tier: "standard"
discount: "0"
results:
- name: classification_result
kind: switch
from: classify_event
value: $steps.premium_process.outputs
Example 4: from Pointing at a Top-Level Workflow
When the emitting construct is itself a top-level workflow (not a nested step):
workflows:
- workflowId: batch_process
type: loop
items: $outputs.job_queue
steps:
- stepId: run_job
operationRef: execute_job
results:
- name: batch_results
kind: loop
from: batch_process # top-level workflow, no ".stepId"
Name Uniqueness
Result names MUST be unique within results[]. The validator catches duplicates:
results:
- name: my_result
kind: merge
from: wf1.step1
- name: my_result # ← duplicate
kind: loop
from: wf2
# error: results[1].name: duplicate result name "my_result"
Validator: Kind/Type Mismatch
workflows:
- workflowId: my_loop
type: loop
items: $outputs.items
steps:
- stepId: process
operationRef: do_work
results:
- name: bad_result
kind: merge # ← wrong: loop construct, but kind is merge
from: my_loop
# error: results[0].kind: kind "merge" does not match "my_loop" type "loop"
From The Big Fixture
The large fixture declares switch, loop, and merge results from top-level workflows:
result "decision.branch" {
kind = "switch"
from = "wf_switch"
value = "$workflows.wf_switch.outputs.selectedPath"
}
result "containment.loop" {
kind = "loop"
from = "wf_loop"
value = "$workflows.wf_loop.outputs.containmentResults"
}
result "merge.summary" {
kind = "merge"
from = "wf_merge"
value = "$workflows.wf_merge.outputs.summary"
}
Full context: testdata/big/big.hcl.
← Triggers and Route Dispatch | Next: Success Criteria and Actions →