Skip to main content

Triggers & Control Flow

These node types control the shape of a workflow's DAG rather than calling an external service: pausing, branching, looping, fanning in, calling other workflows, asserting outputs, and shaping synchronous webhook responses.

timer

Pauses the workflow for a fixed duration. Backed by Temporal's durable timer (workflow.Sleep), so the pause survives worker restarts and deploys without consuming worker resources.

{
"id": "wait_a_bit",
"type": "timer",
"depends_on": ["fetch_data"],
"config": {
"duration": 30,
"unit": "minutes"
}
}
FieldTypeDefaultDescription
durationnumber or expressionLength of the pause, in the unit given by unit. Supports {{...}} expressions.
unitstring"seconds"One of seconds, minutes, hours, days.

Output: { "slept_for": "30m0s", "resumed_at": "2026-06-11T09:00:00Z" }slept_for is the resolved duration (Go duration string) and resumed_at is the UTC timestamp the workflow continued at.

delay

Pauses the workflow either for a relative duration or until a specific point in time. Like timer, it's intercepted at the workflow level and executed via a durable Temporal sleep.

{
"id": "wait_until_business_hours",
"type": "delay",
"depends_on": ["create_ticket"],
"config": {
"operation": "delay_until",
"until": "2026-06-12T09:00:00Z"
}
}
FieldTypeDefaultDescription
operationstring"delay_for"delay_for (relative duration) or delay_until (absolute timestamp).
seconds / minutes / hours / days / months / yearsnumber0Used with delay_for — components are summed to compute the total duration.
untilstring (expression)Used with delay_until — an ISO-8601/RFC3339 timestamp (or expression resolving to one) to sleep until.
formatstringOptional input format hint for parsing until if it isn't already RFC3339.

Output (delay_for): { "delayed_for_seconds": 1800, "operation": "delay_for" }

Output (delay_until): { "delayed_until": "2026-06-12T09:00:00Z", "delayed_seconds": 12345, "operation": "delay_until" }

loop

Iterates over an array and produces a transformed array as output. Each element is bound to a per-iteration variable that other expressions in transform can reference.

{
"id": "format_leads",
"type": "loop",
"depends_on": ["fetch_leads"],
"config": {
"items": "{{steps.fetch_leads.output.body.records}}",
"item_var": "lead",
"transform": {
"email": "{{lead.email}}",
"name": "{{lead.fields.Name}}"
}
}
}
FieldTypeDefaultDescription
itemsarray (expression)Required. Must resolve to a JSON array.
item_varstring"item"Name used to reference the current element inside transform expressions, e.g. {{item.field}}.
transformobject (optional)A map of output_key -> expression. If set, each element of items is mapped through this object (with item_var bound to that element) to produce the corresponding entry in results. If omitted, each item in items is passed through unchanged.

Output: { "results": [...], "count": <number> }results is the array of transformed (or pass-through) items, one per input element; count is its length.

note

loop performs a single in-place mapping over the array — it does not execute a nested sub-DAG of steps per item. To run a multi-step sub-workflow once per item, pair loop/batch with a subworkflow step (calling the sub-workflow once per element via your own fan-out, or once per batch).

batch

Splits an array into fixed-size chunks — useful for rate-limited APIs or for feeding a fixed number of items at a time into a downstream subworkflow or connector call.

{
"id": "chunk_records",
"type": "batch",
"depends_on": ["fetch_records"],
"config": {
"items": "{{steps.fetch_records.output.rows}}",
"size": 25
}
}
FieldTypeDefaultDescription
itemsarray (expression)Required. Must resolve to a JSON array.
sizenumber10Maximum number of elements per batch. Values <= 0 fall back to the default.

Output: { "batches": [[...], [...], ...], "total": <number>, "batch_count": <number> }batches is an array of arrays (each up to size elements), total is the input array length, and batch_count is the number of batches produced.

condition

Evaluates a single comparison and routes downstream steps along a true or false branch via branch_deps.

{
"id": "is_high_value",
"type": "condition",
"depends_on": ["fetch_lead"],
"config": {
"left": "{{steps.fetch_lead.output.deal_value}}",
"operator": "gt",
"right": 10000
}
}
FieldTypeDefaultDescription
leftany (expression)Left-hand operand. Resolved and stringified for comparison.
operatorstring"eq"One of eq, ne, gt, lt, gte, lte, contains, starts_with, ends_with, is_empty, is_not_empty. gt/lt/gte/lte require numeric operands.
rightany (expression)Right-hand operand. Resolved and stringified for comparison.

Output: { "result": true | false, "branch": "true" | "false" }

Downstream steps gate on this result with branch_deps:

{
"id": "notify_slack",
"type": "slack-send",
"depends_on": ["is_high_value"],
"branch_deps": [{ "step_id": "is_high_value", "branch": "true" }],
"config": { "...": "..." }
}

A step only runs once all of its branch_deps entries match the branch produced by the referenced condition/switch step. If the upstream step took a different branch, the step (and anything that depends only on it) is skipped.

switch

Routes to one of several named branches based on matching a value against a list of cases — a multi-way alternative to condition.

{
"id": "route_by_type",
"type": "switch",
"depends_on": ["fetch_event"],
"config": {
"value": "{{steps.fetch_event.output.body.type}}",
"cases": [
{ "match": "issue.opened", "label": "new_issue" },
{ "match": "issue.closed", "label": "closed_issue" }
]
}
}
FieldTypeDefaultDescription
valueany (expression)The value to match. Resolved and stringified before comparison.
casesarray of { match, label }[]Each case's match is stringified and compared against value. The first match wins, and its label becomes the produced branch.

Output (match found): { "matched": true, "branch": "<label>", "value": "<resolved value>" }

Output (no match): { "matched": false, "branch": "default", "value": "<resolved value>" }

Downstream steps use branch_deps exactly as with condition, matching against the label values from cases (or "default" if nothing matched):

{
"branch_deps": [{ "step_id": "route_by_type", "branch": "new_issue" }]
}

subworkflow

Calls another workflow definition as a child Temporal workflow, waits for it to complete, and returns its output — used to compose smaller workflows into larger ones.

{
"id": "run_enrichment",
"type": "subworkflow",
"depends_on": ["fetch_lead"],
"config": {
"workflow_id": "8f1b2c3d-...-enrichment-wf",
"input_data": {
"email": "{{steps.fetch_lead.output.email}}"
},
"timeout_min": 20
}
}
FieldTypeDefaultDescription
workflow_idstring (expression)Required. UUID of the target workflow definition. The workflow must be status: active.
input_dataobject or string (expression)Optional. If set, this becomes the child workflow's trigger body ({{trigger.body...}} inside the child). If it's a JSON string it is parsed; if parsing fails it's wrapped as {"raw": "<string>"}. If omitted, the child receives the entire parent execution context (steps, vars, trigger, input) as its trigger data.
timeout_minnumber20Maximum time (minutes) to wait for the child workflow to complete.

Output: { "output": { ... }, "execution_id": "<uuid>", "status": "completed" }output is the child workflow's final output map (reference nested fields as {{steps.run_enrichment.output.output.<field>}}), and execution_id is the child's own execution ID (visible in Executions & Monitoring).

If the child workflow fails, the step fails (it is not auto-retried — RetryPolicy: MaximumAttempts: 1). Set continue_on: "error" on the step to let the parent continue with {"error": "..."} as the step output.

merge

Flattens the outputs of several upstream steps into a single map — useful for fan-in after parallel branches, or to gather scattered fields before a single downstream call.

{
"id": "combine_results",
"type": "merge",
"depends_on": ["fetch_user", "fetch_orders"],
"config": {
"steps": ["fetch_user", "fetch_orders"]
}
}
FieldTypeDefaultDescription
stepsarray of strings[]Step IDs whose output maps should be merged. Each must already be present in depends_on.

Output: A single flat map containing every key from each listed step's output map, in list order — later steps' keys overwrite earlier ones if there's a name collision. Steps whose output isn't an object are skipped.

test-runner

Runs a list of assertions against prior step outputs. Typically placed at the end of a workflow (or sub-workflow) to validate results — useful for CI-style regression checks on workflow changes.

{
"id": "verify_output",
"type": "test-runner",
"depends_on": ["call_llm"],
"config": {
"label": "LLM Output Checks",
"stop_on_fail": true,
"assertions": [
{ "name": "has_summary", "path": "{{steps.call_llm.output.text}}", "op": "not_empty" },
{ "name": "status_ok", "path": "{{steps.call_llm.output.status}}", "op": "status_ok" }
]
}
}
FieldTypeDefaultDescription
labelstring"Test Suite"Label included in the summary output.
stop_on_failbooleantrueIf true, the step (and workflow, unless continue_on: "error" is set) fails when any assertion fails. If false, results are recorded but the step still succeeds.
assertionsarray of objects[]Each item: { "name": "...", "path": "<expression>", "op": "<operator>", "expected": <any> }. name defaults to assertion_N; op defaults to not_empty.

Supported op values: not_empty, equals/eq, not_equals/neq, contains, not_contains, starts_with, ends_with, regex, json_valid, type_is (compares against string, number, bool, slice, map, etc.), lt, gt, lte, gte, status_ok (HTTP status is 2xx).

Output:

{
"label": "LLM Output Checks",
"passed": 2,
"failed": 0,
"total": 2,
"results": [
{ "name": "has_summary", "op": "not_empty", "actual": "...", "expected": null, "passed": true }
],
"summary": "LLM Output Checks: 2/2 passed",
"ok": true
}

When stop_on_fail is true and one or more assertions fail, the step returns this same output map alongside an error listing each failed assertion's name and message.

webhook_response

Sends a custom HTTP response back to the caller of a synchronous webhook (POST /webhooks/{workflowId}/sync), letting you control the status code, headers, and body of the workflow's HTTP reply instead of returning the raw final step output.

{
"id": "respond",
"type": "webhook_response",
"depends_on": ["build_result"],
"config": {
"status_code": 200,
"content_type": "json",
"body": {
"ok": true,
"result": "{{steps.build_result.output.summary}}"
},
"headers": {
"X-Request-Id": "{{trigger.headers.x-request-id}}"
}
}
}
FieldTypeDefaultDescription
status_codenumber200HTTP status code returned to the caller.
content_typestring"json"One of json, text, html. Sets the Content-Type response header (application/json, text/plain, text/html) unless overridden in headers.
bodyany (expression)The response body. For json, the resolved value is encoded as JSON directly; for text/html, it's stringified.
headersobject{}Additional response headers, each value resolved as an expression.

Output: { "sent": true | false, "status_code": <number> }sent is false if no caller is currently waiting on this execution (e.g. the workflow wasn't invoked via /sync, or the response was already sent by an earlier step).

A workflow can include at most one effective webhook_response per execution path — only the first one reached delivers the reply; subsequent calls return sent: false.

Next