Workflow Syntax

Workflows are defined in YAML (or JSON). A workflow file contains a main workflow and optional subworkflows. Steps execute sequentially by default.

Basic structure

main:
  params: [args]
  steps:
    - step_name:
        <step_type>: <value>
    - another_step:
        <step_type>: <value>

Step types

assign

Set variables. Up to 50 assignments per step. Assignments within a step execute sequentially -- later assignments can reference earlier ones.

- init:
    assign:
      - name: "Alice"
      - age: 30
      - greeting: '${"Hello, " + name}'
      - items: [1, 2, 3]
      - config:
          debug: true
          level: "info"

Index and key assignment:

- update:
    assign:
      - items[0]: "first"
      - config.debug: false
      - config["new_key"]: "value"

Assigning to a nested map path creates intermediate maps: myMap.a.b.c: "deep" creates {a: {b: {c: "deep"}}}.

call

Call HTTP endpoints, standard library functions, or subworkflows.

HTTP call:

- get_data:
    call: http.get
    args:
      url: http://localhost:9090/api/data
      headers:
        Authorization: "Bearer ${token}"
      query:
        limit: "10"
      timeout: 30
    result: response

Standard library call:

- log_it:
    call: sys.log
    args:
      data: "Processing complete"
      severity: "INFO"

Subworkflow call:

- process:
    call: my_subworkflow
    args:
      input: ${data}
    result: output

The result field stores the return value in a variable. See the Standard Library for all available functions.

switch

Conditional branching. Conditions are evaluated top-to-bottom; the first match executes. Up to 50 conditions.

- check:
    switch:
      - condition: ${age >= 18}
        steps:
          - adult:
              assign:
                - category: "adult"
      - condition: ${age >= 13}
        next: teenager_step
      - condition: true  # default/fallback
        steps:
          - child:
              assign:
                - category: "child"

Each condition entry can have next (jump), steps (inline steps), or other inline step content (assign, call, return, raise).

If no condition matches and the switch step has no next, execution continues to the next sequential step.

for

Iterate over lists, map keys (via keys()), or ranges.

List iteration:

- loop:
    for:
      value: item
      in: ${my_list}
      steps:
        - process:
            call: http.post
            args:
              url: http://localhost:9090/process
              body:
                item: ${item}

With index:

- loop:
    for:
      value: item
      index: i
      in: ${my_list}
      steps:
        - log:
            call: sys.log
            args:
              text: '${"Item " + string(i) + ": " + item}'

Range (inclusive both ends):

- loop:
    for:
      value: i
      range: [1, 10]
      steps:
        - process:
            assign:
              - total: ${total + i}

Map iteration:

- loop:
    for:
      value: key
      in: ${keys(my_map)}
      steps:
        - use:
            assign:
              - val: ${my_map[key]}

Loop control: Use next: break to exit the loop, next: continue to skip to the next iteration.

Scoping: Variables created inside a for loop do not exist after the loop ends. Variables from the parent scope that are modified inside the loop retain their changes. The loop variable (value) and index variable are scoped to the loop body.

parallel

Execute branches concurrently or iterate in parallel.

Parallel branches:

- fetch_all:
    parallel:
      shared: [results]
      branches:
        - get_users:
            steps:
              - fetch:
                  call: http.get
                  args:
                    url: http://localhost:9090/users
                  result: users
              - save:
                  assign:
                    - results: ${map.merge(results, {"users": users.body})}
        - get_orders:
            steps:
              - fetch:
                  call: http.get
                  args:
                    url: http://localhost:9091/orders
                  result: orders
              - save:
                  assign:
                    - results: ${map.merge(results, {"orders": orders.body})}

Parallel for:

- process_batch:
    parallel:
      shared: [processed]
      for:
        value: item
        in: ${items}
        steps:
          - process:
              call: http.post
              args:
                url: http://localhost:9090/process
                body: ${item}

Options:

FieldDescription
sharedVariables from parent scope writable by branches (must be declared before the parallel step)
concurrency_limitMax concurrent branches/iterations (default: up to 20)
exception_policyunhandled (default -- abort on first error) or continueAll (collect up to 100 errors)

Shared variables: Individual reads and writes are atomic, but compound operations like total: ${total + 1} are not atomic as a unit -- race conditions can occur. Variables not in shared are read-only copies within each branch.

Limits: 10 branches per step, 20 max concurrent, nesting depth 2.

try / except / retry

Error handling with optional retry. See Error Handling for the full error model.

- safe_call:
    try:
      call: http.get
      args:
        url: http://localhost:9090/unstable
      result: response
    except:
      as: e
      steps:
        - handle:
            assign:
              - response:
                  error: ${e.message}

With retry:

- retry_call:
    try:
      call: http.get
      args:
        url: http://localhost:9090/flaky
      result: response
    retry:
      predicate: ${http.default_retry}
      max_retries: 5
      backoff:
        initial_delay: 1
        max_delay: 30
        multiplier: 2
    except:
      as: e
      steps:
        - fallback:
            return: "service unavailable"

Retries re-execute the entire try block from the beginning, not just the failed step.

raise

Throw an error. Accepts a string or a map.

# String error
- fail:
    raise: "something went wrong"

# Structured error
- fail:
    raise:
      code: 400
      message: "invalid input"
      tags: ["ValidationError"]

# Re-raise a caught error
- rethrow:
    raise: ${e}

return

Return a value from the current workflow or subworkflow.

- done:
    return: ${result}

In main, this terminates the execution and the value becomes the execution result. In subworkflows, the value is returned to the caller.

next

Jump to another step.

- check:
    switch:
      - condition: ${x > 10}
        next: big_number
- small:
    assign:
      - category: "small"
    next: done
- big_number:
    assign:
      - category: "big"
- done:
    return: ${category}

Special targets: end (stop workflow), break (exit loop), continue (next iteration).

steps

Nested step grouping for organization. Variables share the parent scope.

- outer:
    steps:
      - inner1:
          assign:
            - x: 1
      - inner2:
          assign:
            - y: 2

Subworkflows

Define reusable subworkflows alongside main:

main:
  steps:
    - call_helper:
        call: add_numbers
        args:
          a: 10
          b: 20
        result: sum
    - done:
        return: ${sum}

add_numbers:
  params: [a, b]
  steps:
    - calc:
        return: ${a + b}

Key points:

  • main accepts a single parameter (the execution argument as a map)
  • Subworkflows accept multiple named parameters with optional defaults: params: [required, optional: "default"]
  • Subworkflows can be called from call steps (named args) or expressions (positional args): ${add_numbers(10, 20)}
  • Variables are isolated per subworkflow -- a subworkflow cannot access the caller's variables
  • Subworkflows can call other subworkflows and themselves (recursion)
  • Maximum call stack depth: 20

Parameters

# main receives execution argument as a single map
main:
  params: [args]
  steps:
    - use:
        return: ${args.name}

# Subworkflow with named params and defaults
greet:
  params: [first_name, last_name: "Unknown"]
  steps:
    - build:
        return: '${"Hello " + first_name + " " + last_name}'

When triggering an execution, the argument is a JSON-encoded string:

curl -X POST .../executions \
  -H "Content-Type: application/json" \
  -d '{"argument": "{\"name\": \"Alice\", \"age\": 30}"}'

If main has no params, the execution argument must be null or omitted.