Feature 3: Runtime Expression Grammar
← Six Structural Constructs | Next: Triggers and Route Dispatch →
UWS uses runtime expression strings in control-flow fields (when, forEach, wait, items, batchSize), in criterion conditions, and in the string values of outputs maps. The expression language is deliberately small and normative — a runtime that implements it verbatim is portable by construction.
Expression Sources
Every expression references data through a leading $ sigil. A dotted suffix .<segment> walks into the resolved value.
| Source | Meaning |
|---|---|
$response.statusCode |
HTTP response status code of the current operation |
$response.body |
HTTP response body of the current operation |
$response.body#/json/pointer |
RFC 6901 JSON Pointer into the response body |
$response.headers.<name> |
HTTP response header value by name |
$outputs.<name> |
Same-scope output declared by the enclosing operation, workflow, or step |
$outputs.<name>.<path> |
Dot-walk into a structured output value |
$steps.<stepId>.outputs.<name> |
Output of a named sibling step |
$steps.<stepId>.outputs.<name>.<path> |
Dot-walk into a sibling step output |
$variables.<name> |
Document-scope variable from variables or components.variables |
$variables.<name>.<path> |
Dot-walk into a structured variable value |
$trigger |
The payload delivered by the enclosing trigger |
$trigger.<path> |
Dot-walk into the trigger payload |
A resolved value may be any JSON type: string, number, boolean, null, object, or array.
Accessing Response Data
The most common expression source — reading data out of an HTTP response:
operationId: search_products
sourceDescription: catalog_api
openapiOperationId: searchProducts
outputs:
# Whole body
raw_response: $response.body
# Top-level field
total_count: $response.body.total
# JSON Pointer — first item's id field
first_id: $response.body#/items/0/id
# Nested field via dot-walk
first_name: $response.body.items.0.name
# HTTP status code
status: $response.statusCode
# Response header
next_page: $response.headers.X-Next-Page
Using a response value to conditionally skip the next step:
- stepId: fetch_user
operationRef: get_user
outputs:
active: $response.body.active
- stepId: send_email
operationRef: send_welcome
when: $steps.fetch_user.outputs.active == true
Cross-Step References with $steps
$steps.<stepId>.outputs.<name> reads the named output of a sibling step within the same workflow:
workflowId: enrich_order
type: sequence
steps:
- stepId: load_order
operationRef: get_order
outputs:
order_id: $response.body.id
user_id: $response.body.userId
total: $response.body.total
- stepId: load_user
operationRef: get_user
request:
path:
userId: $steps.load_order.outputs.user_id
- stepId: apply_discount
operationRef: compute_discount
request:
body:
orderId: $steps.load_order.outputs.order_id
userTier: $steps.load_user.outputs.tier
baseTotal: $steps.load_order.outputs.total
Dot-walking into a structured output:
outputs:
street: $steps.load_user.outputs.address.street
city: $steps.load_user.outputs.address.city
Dot-walk segments MUST match [A-Za-z0-9_-]+. A property name containing . cannot be accessed this way — use a JSON Pointer on $response.body or a dedicated output instead.
Document-Scope Variables with $variables
Variables declared at the top level or in components.variables are available everywhere:
variables:
api_env: production
page_size: 20
config:
region: us-west-2
timeout: 30
operations:
- operationId: list_items
sourceDescription: catalog_api
openapiOperationId: listItems
request:
query:
env: $variables.api_env
pageSize: $variables.page_size
region: $variables.config.region
Trigger Payload Access with $trigger
Inside workflows started by a trigger dispatch, $trigger holds the inbound payload:
triggers:
- triggerId: order_webhook
outputs: [created, updated]
routes:
- output: created
to: [handle_new_order]
workflows:
- workflowId: handle_new_order
type: sequence
steps:
- stepId: save_order
operationRef: create_order
request:
body:
externalId: $trigger.orderId
amount: $trigger.total
currency: $trigger.currency
- stepId: notify
operationRef: send_confirmation
when: $trigger.notify == true
request:
body:
email: $trigger.customer.email
Comparison Operators
Inside when, Criterion.condition (when type is simple), and other boolean expression fields, UWS defines six comparison operators:
== != < <= > >=
Rules:
- Operands MUST be of the same JSON type — no implicit coercion.
- == and != apply to strings, numbers, booleans, and null.
- <, <=, >, >= apply to strings (lexicographic) and numbers only.
- A single space surrounds the operator; == / != / <= / >= are always two-character tokens.
# Number comparison
when: $response.body.count > 0
when: $response.statusCode == 200
when: $variables.retries <= 3
# String comparison
when: $response.body.status == "active"
when: $trigger.event != "ping"
# Null check
when: $steps.load.outputs.result != null
JSON Pointer Fragments
A #-prefixed RFC 6901 JSON Pointer can be appended to $response.body to address deeply nested values:
outputs:
# First item in an array
first_item: $response.body#/items/0
# Nested object field
street: $response.body#/address/street
# Key with a slash in its name (slash escaped as ~1)
value: $response.body#/metrics~1rate
Inside a Criterion with type: jsonpath, the context field uses the same pointer form to select the value to test.
Output Dot-Walk and null Semantics
If a segment in a dot-walk path does not resolve — the property is absent, the index is out of bounds, or the segment is applied to a scalar — the expression evaluates to null:
# If body.items is absent or empty, first_id resolves to null
outputs:
first_id: $response.body.items.0.id
# Guard against null with a criterion
successCriteria:
- condition: $outputs.first_id != null
Normative ABNF Grammar (§5.6)
The full grammar is defined in §5.6 of the spec. Key productions:
expression = condition / source-expr
condition = source-expr SP op SP operand
op = "==" / "!=" / "<=" / ">=" / "<" / ">"
operand = source-expr / literal
source-expr = response-expr / outputs-expr / steps-expr / variables-expr / trigger-expr
response-expr = "$response.statusCode"
/ "$response.body" [ json-pointer ]
/ "$response.headers." header-name
outputs-expr = "$outputs." name [ "." path ]
steps-expr = "$steps." identifier ".outputs." name [ "." path ]
variables-expr = "$variables." name [ "." path ]
trigger-expr = "$trigger" [ "." path ]
path = segment *( "." segment )
segment = 1*id-char
id-char = ALPHA / DIGIT / "_" / "-"
literal = json-string / json-number / json-bool / json-null
Implementation Extensions
Richer features — boolean connectives (&&, ||, !), arithmetic, function calls — are implementation-defined. A conforming UWS core MUST NOT require them for documents using only the normative grammar. Documents that depend on extended syntax SHOULD declare the dependency via x-uws-operation-profile.
# This is UWS-core expression (normative — all runtimes support it):
when: $response.statusCode == 200
# This requires an implementation extension (non-portable):
when: $response.statusCode == 200 && $response.body.count > 0
From The Big Fixture
The large fixture uses expressions in request bindings, controls, and outputs:
operation "run_llm_primary" {
dependsOn = ["fetch_ticket", "load_customer"]
when = "$steps.step_collect_context.outputs.enabled == true"
forEach = "$variables.regions"
wait = "$signals.runtime_slot_available"
outputs = {
audit = "$response.body.auditId"
result = "$response.body.result"
}
request {
body {
incidentId = "$inputs.incidentId"
runtime = "llm"
variant = "primary"
}
}
}
Full context: testdata/big/big.hcl.
← Six Structural Constructs | Next: Triggers and Route Dispatch →