Skip to main content

Tasks Reference

A task is the unit a workflow is built from: main is one task, and containers and control flow hold more tasks. Every task has exactly one kind key, which says what it does, plus any of the shared keys task_name, skip_if, props, output and on_error.

This page documents every task kind. For the shared keys, expressions, scopes and error handling, see the Workflowspec Reference. For the activities an activity task can call, see the Activity Catalog.

Kinds at a glance​

KindCategoryResult _
activityprimitiveThe activity's result; a token with async_mode
workflowprimitiveThe child's output; its workflow_id for async / detached
setprimitiveA mapping of the values it stored (task-locals excluded)
raiseprimitivenull when its if is false; otherwise it does not complete
emit_eventprimitivenull
wait_forprimitiveThe event, or null on timeout
requestprimitiveThe reply event; RequestTimeoutError when none arrives
checkpointprimitivenull
state_machineprimitiveThe name of the terminal state reached
rules_engineprimitiveThe derived facts
taskscontainerThe list of child results; the winner's result with join_type: any
loopcontrol flowThe list of iteration results; the winner's result with join_type: any
switchcontrol flowThe result of the case that ran, or null
branchescontrol flowThe result of the branch that ran, or null

_ is the task's raw output. It is visible in the task's output, where output.data (default _) becomes the task's result and output.as stores it:

- task_name: get-user
activity:
type: http.request
input:
method: GET
url: https://httpbin.org/json
output_json: true
output:
data: '{{ _["json"] }}'
as: user

Shared keys​

KeyDescription
task_nameIdentifies the task in logs, traces and tests. Unique within the document
skip_ifA condition. When true, the task does not run and its result is null
propsOptions. execute_mode and join_type configure a tasks container
outputset (ordered writes), data (the result, default _), as (where to store it)
on_errorcatch and do: handle an error raised by this task

Details in Shared task keys.


Primitive tasks​

activity​

Calls an activity: an HTTP request, a SQL query, a shell command, an LLM call, and so on.

activity:
type: <activity type>
input: <mapping>
options:
retry_policy: <retry policy>
execute_locally: <bool>
enable_cache: <bool>
cache_policy: {expire_in_seconds, disable}
async_mode: <bool>
async_event_topic: <topic>
FieldDescription
typeActivity type, such as http.request (required). See the Activity Catalog
inputThe activity's input fields. Evaluated each time the task runs
options.retry_policyTimeouts and retries, laid over the header's default_activity_retry_policy. Field values may be expressions, evaluated each time the task runs. See Retry policy
options.execute_locallyRun the activity in the workflow's own process. Overrides the activity's default only when set
options.enable_cacheCache the activity's result
options.cache_policyexpire_in_seconds (cache lifetime), disable
options.async_modeDo not wait: return a token now, and publish the result later as an event
options.async_event_topicTopic of that event. Default default

Result _: the activity's result. With async_mode, a token string: the result arrives later as an event on async_event_topic whose event_type is that token. Collect it with wait_for.

header:
schema_version: 2.0.0
workflow_name: activity-task

output:
echoed: '{{ echoed }}'

main:
task_name: post-order
activity:
type: http.request
input:
method: POST
url: https://httpbin.org/post
json_data:
order_id: ORD-1
output_json: true
raise_for_status: true
options:
retry_policy:
max_attempts: 3
timeout_sec: 30
output:
data: '{{ _["json"]["json"] }}'
as: echoed

Some activities already default to local execution, so execute_locally is rarely needed. Setting it to false on a selenium.* or playwright.* activity breaks browser sessions; see Activities.

An asynchronous activity, collected later:

header:
schema_version: 2.0.0
workflow_name: async-activity-task

output:
status: '{{ status }}'

main:
tasks:
- task_name: slow-lookup
activity:
type: builtin.delay
input:
duration: 1s
options:
async_mode: true
async_event_topic: lookup_events
output:
as: lookup_token

- task_name: await-lookup
wait_for:
event:
topic: lookup_events
event_type: '{{ lookup_token }}'
timeout_sec: 30
output:
data: '{{ _["metadata"].get("status") if _ else "timed-out" }}'
as: status

workflow​

Runs a child workflow, or a function of the same document.

workflow:
ref: <reference> # exactly one of ref, function, contents, contents_yaml
function: <function name>
contents: [<wfspec>, ...]
contents_yaml: <string> | [<string>, ...]
mode: inline | sync | async | detached
input: <mapping>
options:
retry_policy: <retry policy>
debug_mode: <bool>
debug_info: <any>
enable_otel_trace: <bool>
execute_mode: workflow | standalone-activity | in-memory
entity_workflow_args: <any>
FieldDescription
refA published workflow, by name. A string or a mapping (below)
functionA function from this document's functions
contentsChild wfspecs written inline as YAML. The first is the entry point
contents_yamlThe same as YAML text (a string or a list of strings), for example built or chosen at runtime
modeHow the child runs. Default inline
inputThe child's input. The child's context is built from it, as for a top-level run
options.retry_policyTimeouts and retries of a sync child. Field values may be expressions. See Retry policy
options.debug_mode, options.debug_infoRun the child in debug mode, with optional debug data
options.enable_otel_traceEmit OpenTelemetry traces for the child
options.execute_modeHow the child is dispatched: workflow (default; each activity dispatched separately), standalone-activity (the whole child runs as one activity, efficient for large-data or CPU-bound work), or in-memory
options.entity_workflow_argsEntity-workflow (signal-with-start) configuration
modeBehaviour
inlineRuns inside the parent's execution: same workflow id, no separate history. The parent waits. The cheapest mode
syncA separate workflow with its own id, history, retries and timeouts. The parent waits for the result
asyncStarted and not waited for. Cancelled if the parent finishes first
detachedStarted and not waited for. Outlives the parent

Every mode, inline included, gives the child a fresh context made only of its input: it cannot see or change the caller's variables. Choose between inline and sync for cost and failure isolation, not for data.

Result _: the child's output. For async and detached, the child's workflow_id. A failing child raises its error in the parent, where on_error can handle it.

References. A string ref is a path of /-separated segments: the last is the workflow name, the one before it may be a version pattern (semver, * wildcards allowed), and the rest name the package. A leading / makes the first segment a namespace; without it the caller's namespace is used. A one-segment ref names a workflow in the caller's package.

- workflow:
ref: send-receipt # same namespace and package, latest version
- workflow:
ref: /billing/invoices/2.*/send-receipt # namespace / package / version / name
- workflow:
ref: {namespace: billing, name: invoices/send-receipt, version: '2.*'}

ref may be an expression, to pick a published workflow at runtime.

header:
schema_version: 2.0.0
workflow_name: workflow-task

input:
x: 21

output:
doubled: '{{ doubled }}'
started: '{{ started }}'

main:
tasks:
- task_name: double-via-child
workflow:
contents:
- header:
schema_version: 2.0.0
workflow_name: doubler
input:
x: 0
output: '{{ x * 2 }}'
main:
set: []
mode: sync
input:
x: '{{ x }}'
output:
as: doubled # 42

- task_name: start-background-job
workflow:
contents:
- header:
schema_version: 2.0.0
workflow_name: background-job
main:
activity:
type: builtin.delay
input: {duration: 1s}
mode: async
output:
data: '{{ _ is not None }}' # _ is the child's workflow_id
as: started

Calling a function​

function: runs a function of the same document. It always runs inline, so mode must be absent or inline.

header:
schema_version: 2.0.0
workflow_name: function-call-task

functions:
add:
input: {a: 0, b: 0}
output: '{{ a + b }}'
main:
set: []

output: '{{ total }}'

main:
task_name: call-add
workflow:
function: add
input: {a: 1, b: 2}
output:
as: total # 3

For composition patterns (data resolvers, parent and child talking through events, entity workflows), see Child Workflows.


set​

Assigns variables, without calling anything.

set:
- <target>: <value>
- ...

Each item is a mapping with one key: a write target (name, _name, name@, with optional modifiers and a trailing #). Items run in order, and each can read the ones before it. A key written as '{{ name[...] }}' assigns into an existing dict or list (subscript assignment).

Result _: a mapping of the values stored by the non-task-local items, keyed by name with the markers removed. Task-locals written by set stay visible in the task's own output.

header:
schema_version: 2.0.0
workflow_name: set-task

input:
first_name: jon
last_name: doe

output:
greeting: '{{ greeting }}'
stored: '{{ stored }}'

main:
set:
- _name: '{{ first_name }} {{ last_name }}' # task-local
- greeting#: 'Hello, {{ _name.title() }}!' # context, logged
output:
as: stored # {greeting: Hello, Jon Doe!}

An empty set: [] does nothing. It is the usual main of a function or child whose result is computed entirely by its output.


raise​

Fails the task with an error, or stops early with a control signal.

raise:
if: <condition>
type: error | terminate | exit | break | continue
message: <value>
error_type: <name>
FieldDescription
ifRaise only when this condition is true. Without it, the task always raises
typeWhat to raise. Default error
messageThe message. Evaluated
error_typeThe error's type, seen by on_error as error_type. Only with type: error. Default WorkflowError
typeEffect
errorFails the task. on_error handlers can catch it
terminateStops the whole workflow, which completes successfully: its output is evaluated and gets terminate_message
exitLeaves the nearest enclosing tasks container; execution continues after it
breakLeaves the nearest enclosing loop, which keeps the results collected so far
continueEnds the current loop iteration with result null and moves to the next

terminate, exit, break and continue are control signals, not errors: on_error never sees them. exit must sit inside a tasks container and break / continue inside a loop.

Result _: null when if is false. Otherwise the task does not complete normally.

header:
schema_version: 2.0.0
workflow_name: raise-task

input:
first_name:
last_name:

output:
full_name: '{{ full_name }}'

main:
tasks:
- task_name: require-first-name
raise:
if: '{{ not first_name }}'
type: error
error_type: ValidationError
message: first_name is missing

- task_name: stop-without-last-name
raise:
if: '{{ not last_name }}'
type: terminate
message: no last_name, nothing to do

- task_name: build-name
set:
- full_name: '{{ first_name }} {{ last_name }}'

See Errors and control signals.


emit_event​

Publishes an event, to this workflow or to another one.

emit_event:
topic: <topic>
event_type: <name>
data: <any>
metadata: <any>
target_workflow_id: <workflow id>
suppress_error: <bool>
entity_workflow:
ref | contents | contents_yaml: ...
mode: async | detached
input: <mapping>
reply_to: <request event>
FieldDescription
topicThe channel. Default default
event_typeThe event's name within the topic
dataThe payload
metadataExtra information about the event
target_workflow_idThe workflow that receives the event. Default: this workflow
suppress_errortrue: if delivery fails, log it and carry on instead of failing
entity_workflowStart the target workflow if it is not running, then deliver (signal-with-start). Takes a source (ref, contents or contents_yaml), a mode and an input, as in workflow
reply_toAnswer a request: the request event (event in a transition, _ after a wait_for) or its metadata. The event goes back to the requester on its reply topic, carrying its correlation id. Cannot be combined with target_workflow_id, topic or entity_workflow; a value that is not a request event raises ReplyError

Result _: null.

header:
schema_version: 2.0.0
workflow_name: emit-event-task

input:
order_id: ORD-1
notify_workflow_id:

main:
task_name: publish-order-created
emit_event:
topic: order_events
event_type: order_created
target_workflow_id: '{{ notify_workflow_id or __sys_info__.get("workflow_id") }}'
data:
order_id: '{{ order_id }}'
metadata:
priority: high

See Events for how events flow between workflows, activities and clients.


wait_for​

Waits for an event, or sleeps.

wait_for:
event:
topic: <topic>
event_type: <name>
match_expression: <condition>
timeout_sec: <seconds>
FieldDescription
event.topicThe topic to listen on. Default default
event.event_typeOnly events with this type match. May be an expression, evaluated when the wait starts
event.match_expressionA condition evaluated for each candidate event, with event bound to it
timeout_secHow long to wait. Required when there is no event
  • An event is consumed by the first waiter it matches; a second wait_for will not see it.
  • With a literal event.topic, an event that arrives before the wait_for is reached is kept for it. An expression topic is only listened to from its first wait_for on.
  • Without event, wait_for is a durable sleep of timeout_sec.

Result _: the matched event, a mapping with topic, event_type, data, metadata and more. On timeout, null. A timeout is not an error; test _ in the task's output.

header:
schema_version: 2.0.0
workflow_name: wait-for-task

input:
order_id: ORD-1

output: '{{ result }}'

main:
tasks:
- task_name: await-order-shipped
wait_for:
event:
topic: order_events
event_type: shipped
match_expression: '{{ event["data"].get("order_id") == order_id }}'
timeout_sec: 5
output:
data:
timed_out: '{{ _ is None }}'
shipment: '{{ _["data"] if _ else None }}'
as: result

- task_name: pause
wait_for:
timeout_sec: 1

request​

Sends an event to another workflow and waits for its reply.

request:
target_workflow_id: <workflow id>
topic: <topic>
event_type: <name>
data: <any>
metadata: <any>
reply_topic: <topic>
timeout_sec: <seconds>
FieldDescription
target_workflow_idThe workflow to ask. Required
topicThe topic the request is sent on. Default default
event_typeThe request's name within the topic
dataThe payload
metadataExtra information. correlation_id, reply_to and reply_topic are added to it
reply_topicThe topic the reply is awaited on. Default moco.reply
timeout_secHow long to wait for the reply. Without it the request waits forever (validator warning)
  • The reply is matched by the correlation id the engine adds, so no match_expression is needed and concurrent requests each get their own reply.
  • The receiver answers with emit_event and reply_to.

Result _: the reply event, a mapping with topic, event_type, data, metadata and more. With no reply within timeout_sec the task raises RequestTimeoutError; catch it with on_error.

header:
schema_version: 2.0.0
workflow_name: request-task

input:
pricing_id:
sku: A-1

output: '{{ quote }}'

main:
task_name: get-quote
request:
target_workflow_id: '{{ pricing_id }}'
topic: pricing
event_type: get_quote
data:
sku: '{{ sku }}'
timeout_sec: 30
output:
data: '{{ _["data"] }}'
as: quote
on_error:
catch: true
do:
set:
- quote: null

See Request / Reply for the whole exchange.


checkpoint​

Restarts the run with its state saved (continue-as-new), when the runtime suggests it. On Temporal, a long-running workflow's event history grows with every step; a checkpoint starts a fresh history before it gets too large.

checkpoint:
serialize_context: <bool> # default true
enforce: <bool> # default false
FieldDescription
serialize_contextSave the workflow context, so the restarted run starts with it. Default true
enforceRestart even if the runtime has not suggested it. For testing

After the restart, main runs again from the beginning with the saved context, so the spec must skip work that is already done. Container-scoped and task-local values are not saved. checkpoint does nothing on the in-memory runtime. If main is a single state_machine, use a state's checkpoint_policy instead: the machine resumes in the right state by itself.

Result _: null.

header:
schema_version: 2.0.0
workflow_name: checkpoint-task

context:
processed: 0

input:
total: 1000

output:
processed: '{{ processed }}'

main:
loop:
in: '{{ range(processed, total) }}' # resumes where the previous run stopped
foreach: n
do:
tasks:
- task_name: process
set:
- processed: '{{ n + 1 }}'
- task_name: maybe-continue-as-new
skip_if: '{{ processed % 100 != 0 }}'
checkpoint:
serialize_context: true

state_machine​

An event-driven finite state machine: states, and transitions between them triggered by events, timers or conditions. See State Machines for a full guide.

state_machine:
initial_state: <state name>
states: [<state>, ...]
transitions: [<transition>, ...]
event_source_topic: <topic>
timeout_sec: <seconds>

state:
name: <name>
description: <text>
is_terminal: <bool>
on_enter: <task>
on_exit: <task>
timeout_sec: <seconds>
max_timeout_attempts: <int>
timers: [{name, timeout_sec, max_timeout_attempts}, ...]
checkpoint_policy: {timeout_sec, event_count, serialize_context, auto_resume, enforce}

transition:
from_state: <state name> | [<state name>, ...] | null
to_state: <state name> | null
trigger:
event_type: <name>
condition: <condition>
action: <task>
FieldDescription
initial_stateThe state the machine starts in (required)
statesThe states (required)
transitionsThe transitions
event_source_topicThe topic the machine reads events from
timeout_secTime limit for the whole machine
State fieldDescription
nameThe state's name (required)
is_terminalThe machine stops when it enters this state
on_enter / on_exitA task run when the state is entered / left
timeout_sec, max_timeout_attemptsAn unnamed timer: fires sys.timer after timeout_sec, up to max_timeout_attempts times
timersNamed timers: each fires sys.timer.<name>, and sys.timer.<name>.final when its attempts run out
checkpoint_policyCheckpoint while the machine waits in this state. See below
Transition fieldDescription
from_stateThe state(s) it leaves from. null or omitted: any state (a global trigger)
to_stateThe state it enters. null: an internal transition, which runs action without leaving the state (no on_exit / on_enter)
trigger.event_typeThe event that fires it. May be an expression. Without it, the transition is automatic: checked as soon as the state is entered, and it must have a to_state
trigger.conditionA condition that must also hold
trigger.actionA task run when the transition fires

Inside on_enter, on_exit, action and condition, the workflow context and event (the triggering event) are visible; container scopes around the state machine are not. __sys_info__["state_machine"] holds name (the task's task_name), current_state, and the checkpoint fields below.

Result _: the name of the terminal state reached.

header:
schema_version: 2.0.0
workflow_name: state-machine-task

context:
approver: ''

input:
amount: 800

output:
final_state: '{{ final_state }}'
approver: '{{ approver }}'

main:
task_name: approval
state_machine:
initial_state: routing
timeout_sec: 86400
states:
- name: routing
- name: manager_review
timers:
- name: remind
timeout_sec: 3600
max_timeout_attempts: 24
- name: approved
is_terminal: true
on_enter:
set:
- _log#: 'approved by {{ approver }}'
- name: rejected
is_terminal: true
transitions:
# automatic: no event_type; the first satisfied one wins
- from_state: routing
to_state: approved
trigger:
condition: '{{ amount < 1000 }}'
action:
set:
- approver: system
- from_state: routing
to_state: manager_review

# internal transition: stay in the state, run the action on each timer tick
- from_state: manager_review
to_state:
trigger:
event_type: sys.timer.remind
action:
set:
- _log#: 'reminder: {{ amount }} awaits a manager'

# event-driven
- from_state: manager_review
to_state: approved
trigger:
event_type: approve
action:
set:
- approver: '{{ event["data"].get("by", "manager") }}'
- to_state: rejected # from any state
trigger:
event_type: reject
output:
as: final_state

checkpoint_policy​

Declared on a state. The runtime checkpoints while the machine sits in that state and resumes it there afterwards, so the spec does not have to describe recovery. Allowed only when main is the state_machine task.

header:
schema_version: 2.0.0
workflow_name: checkpoint-policy-task

main:
task_name: monitor
state_machine:
initial_state: idle
states:
- name: idle
checkpoint_policy:
timeout_sec: 300
event_count: 100
- name: done
is_terminal: true
transitions:
- from_state: idle
to_state: done
trigger:
event_type: stop
FieldDefaultDescription
timeout_secnoneCheckpoint after this long in the state
event_countnoneCheckpoint after this many events handled in the state
serialize_contexttrueSave the workflow context
auto_resumetrueResume in this state, skipping its on_enter. false restarts the machine at initial_state
enforcefalseCheckpoint even if the runtime has not suggested it. For testing

At least one of timeout_sec and event_count is required; with both, whichever trips first wins. An event counts toward event_count when a transition matched it in this state and the state did not change: internal transitions, self-transitions, and events whose condition was false. Unmatched events do not count, and moving to another state resets the count. On resume the state's on_enter is skipped (it already ran) and its timers restart from their full duration.

Opting out of auto-resume. auto_resume: false keeps the checkpoint but restarts the machine at initial_state, with on_enter running normally. Use it when an earlier state has a side effect the new run needs again, typically an init state that starts an async_mode activity feeding the machine's topic: activity handles do not survive a checkpoint. The state the checkpoint fired in stays readable, so init can redo its side effect and route straight back:

header:
schema_version: 2.0.0
workflow_name: checkpoint-auto-resume-task

main:
task_name: price-monitor
state_machine:
initial_state: init
event_source_topic: monitor_events
states:
- name: init
on_enter:
task_name: subscriber # restarted on every run
activity:
type: builtin.delay
input: {duration: 1s}
options:
async_mode: true
async_event_topic: monitor_events
- name: warmup
- name: monitoring
checkpoint_policy:
event_count: 100
auto_resume: false
- name: done
is_terminal: true
transitions:
# Listed first: the first satisfied automatic transition wins.
- from_state: init
to_state: monitoring
trigger:
condition: '{{ __sys_info__.get("state_machine", {}).get("checkpointed_from_state") == "monitoring" }}'
- from_state: init
to_state: warmup
- from_state: warmup
to_state: monitoring
- from_state: monitoring
to_state: done
trigger:
event_type: stop
__sys_info__["state_machine"] keyDescription
nameThe state machine task's task_name, or unnamed_state_machine
current_stateThe state the machine is in. Updated before a state's on_enter runs; during a transition's action it is still the source state
checkpointed_from_stateThe state the last checkpoint fired in, whether or not the machine resumed there
resumed_from_stateThe state the machine jumped back into after a checkpoint. null under auto_resume: false

Both checkpoint keys are null on a first run. The state_machine key is removed when the machine completes, and a nested machine restores the outer values when it exits.


rules_engine​

Evaluates rules over a working memory of facts, either forward (data-driven) or backward (goal-driven). See Rules Engine for a full guide, including live mode.

rules_engine:
input:
facts: <mapping>
run_mode: forward | backward
fact_resolvers: {<fact name>: <task>, ...}
terminate_facts: [<fact name>, ...]
keep_alive: <bool>
fact_source_topic: <topic>
timeout_sec: <seconds>
max_iterations: <int>
rules:
- id: <id>
name: <text>
description: <text>
if:
with_facts: [<fact name>, ...]
expression: <condition>
then:
set_facts: [{<fact name>: <value or task>}, ...]
actions: [<task>, ...]
FieldDescription
input.factsThe initial facts. Evaluated. In backward mode, give a goal fact the value null
input.run_modeforward (default) or backward
input.fact_resolversA task per fact that produces the fact on demand
input.terminate_factsStop once all of these facts are derived
input.keep_aliveLive mode: keep the session open and re-evaluate as facts arrive as events
input.fact_source_topicIn live mode, the topic carrying set_facts events. Default default
input.timeout_secTime limit of a live session
input.max_iterationsSafety limit on evaluation cycles in live mode. Default 1000
rules[].idUnique rule id (required)
rules[].if.with_factsThe facts the rule reads
rules[].if.expressionA condition over the facts, which are bound as bare names (a . in a fact name becomes _)
rules[].then.set_factsFacts to assert. A plain value is asserted as written, not evaluated; a task value is run and its result asserted
rules[].then.actionsTasks to run when the rule fires. They run against the workflow context, not the facts

Result _: the derived facts, those asserted by rules. The seeded facts are not included.

header:
schema_version: 2.0.0
workflow_name: rules-engine-task

input:
credit_score: 720
employment_years: 7

output: '{{ derived }}'

main:
task_name: classify-applicant
rules_engine:
input:
facts:
credit_score: '{{ credit_score }}'
employment_years: '{{ employment_years }}'
run_mode: forward
rules:
- id: credit_tier_good
if:
with_facts: [credit_score]
expression:
and:
- '{{ credit_score >= 650 }}'
- '{{ credit_score < 750 }}'
then:
set_facts:
- credit_tier: good
- id: stable_employment
if:
with_facts: [employment_years]
expression: '{{ employment_years >= 5 }}'
then:
set_facts:
- employment_stable: true
actions:
- set:
- _log#: stable employment
output:
as: derived # {credit_tier: good, employment_stable: true}

Container​

tasks​

Runs a list of child tasks, one after another or all at once.

props:
execute_mode: sequential | parallel # default sequential
join_type: all | any # default all; parallel only
tasks:
- <task>
- ...
propsBehaviourResult _
execute_mode: sequential (default)Children run in order. An uncaught child error stops the container. A raise: exit leaves itList of child results, in order
execute_mode: parallel, join_type: all (default)All children start at once. The first uncaught error fails the container and cancels the restList of child results, in declaration order
execute_mode: parallel, join_type: anyAll children start at once. The first to succeed wins and the rest are cancelled. Fails only if every child failsThe winning child's result

The container opens a container scope: values written as name@ inside it are visible to later children and vanish when it ends. In parallel mode, each child works on its own copy of the enclosing container scopes, and writes to the workflow context are applied as children finish.

header:
schema_version: 2.0.0
workflow_name: tasks-container

output: '{{ result }}'

main:
tasks:
- task_name: start
set:
- started_at@: '{{ now() }}' # visible to later siblings only

- task_name: fetch-in-parallel
props:
execute_mode: parallel
join_type: all
tasks:
- task_name: fast
activity:
type: builtin.delay
input: {duration: 1s}
output:
data: fast done
- task_name: slow
activity:
type: builtin.delay
input: {duration: 2s}
output:
data: slow done
output:
as: results # [fast done, slow done]

- task_name: summarise
set:
- result:
started_at: '{{ started_at }}'
results: '{{ results }}'

Control flow​

loop​

Runs a task once for each item of a collection.

loop:
in: <iterable>
foreach: <name> # default item
index: <name>
execute_mode: sequential | parallel # default sequential
join_type: all | any # default all; parallel only
do: <task>
FieldDescription
inThe collection: a list, range(...), dict.items(), any iterable. Evaluated once, before the first iteration (required)
foreachName of the loop variable. Default item
indexName of a 0-based position variable. Optional
execute_modeHow the iterations run. Default sequential
join_typeWith parallel iterations: all (default) or any (the first to succeed wins)
doThe task run for each item (required)
  • Each iteration opens a fresh container scope holding foreach and index, so name@ values are private to the iteration, even in parallel.
  • execute_mode and join_type are keys of loop, not props. If do is a tasks container, its own props govern its children within each iteration, independently.
  • A nested loop must use different foreach / index names. The outer loop's variable stays visible inside.
  • raise: continue ends the current iteration (result null); raise: break ends the loop.
  • foreach and index cannot start with _ or be reserved names.

Result _: the list of iteration results, in input order. An iteration's result is the result of its do task, so put an output on do to choose what each item contributes. With join_type: any, the first successful iteration's result.

header:
schema_version: 2.0.0
workflow_name: loop-task

input:
orders:
- {id: A, units: 2, price: 10.0}
- {id: B, units: 1, price: 40.0}

output:
lines: '{{ lines }}'
grid: '{{ grid }}'

main:
tasks:
- task_name: price-each-order
loop:
in: '{{ orders }}'
foreach: order
index: i
execute_mode: parallel
do:
task_name: price-one
set:
- _amount: '{{ order["units"] * order["price"] }}'
output:
data:
line: '{{ i + 1 }}'
id: '{{ order["id"] }}'
amount: '{{ _amount }}'
output:
as: lines

- task_name: outer
loop:
in: '{{ ["x", "y"] }}'
foreach: letter
do:
task_name: inner
loop:
in: '{{ [1, 2] }}'
foreach: number
do:
set: []
output:
data: '{{ letter }}{{ number }}'
output:
as: grid # [[x1, x2], [y1, y2]]

switch​

Compares one value against a list of cases and runs the first that matches.

switch:
value: <value>
cases:
- match: <value or list>
do: <task>
default: <task>
FieldDescription
valueThe value to compare. Evaluated once (required)
cases[].matchEvaluated. A list matches when value is in it; anything else matches with ==
cases[].doThe task to run when this case matches
defaultThe task to run when no case matches. Optional

Result _: the result of the task that ran, or null if none did.

header:
schema_version: 2.0.0
workflow_name: switch-task

input:
kind: wholesale
amount: 1200

output: '{{ discount }}'

main:
task_name: pick-discount
switch:
value: '{{ kind }}'
cases:
- match: retail
do:
set:
- discount: 0
- match: [wholesale, partner]
do:
set:
- discount: '{{ 0.15 if amount > 1000 else 0.10 }}'
default:
raise:
type: error
error_type: ValidationError
message: 'unknown order kind {{ kind }}'

branches​

Runs the first branch whose condition is true: if, then any number of elif, then an optional else.

branches:
- if: <condition>
do: <task>
- elif: <condition>
do: <task>
- else:
do: <task>
FieldDescription
ifThe first item's condition (required on the first item)
elifA later item's condition
elseWritten with no value. At most one, and it must be last
doThe task the item runs

Conditions are evaluated in order until one is true. Each must evaluate to a bool.

Result _: the result of the branch that ran, or null if none did.

header:
schema_version: 2.0.0
workflow_name: branches-task

input:
amount: 5000

output: '{{ route }}'

main:
task_name: route-purchase
branches:
- if: '{{ amount < 1000 }}'
do:
set:
- route: auto
- elif:
and:
- '{{ amount >= 1000 }}'
- '{{ amount <= 10000 }}'
do:
set:
- route: manager
- else:
do:
set:
- route: executive

For a guard on a single task, skip_if is shorter than a one-branch branches.


Next Steps​