DOC_INDEX
THEME
DOCS/Example Cookbook/Loops Examples

Loops Examples

OrchStep examples for loops, iteration, and loop-until patterns


basic loop

Basic Loop Iteration

# Example: Basic Loop Iteration
# OrchStep supports looping over lists, counts, and ranges.
# Each iteration has access to loop metadata like index and position.
#
# Key concepts:
#   - "loop:" accepts a list variable, a count, or a range
#   - {{ loop.item }}   -- current item value
#   - {{ loop.index }}  -- zero-based index (0, 1, 2, ...)
#   - {{ loop.index1 }} -- one-based index (1, 2, 3, ...)
#   - {{ loop.first }}  -- true on the first iteration
#   - {{ loop.last }}   -- true on the last iteration
#   - {{ loop.length }} -- total number of items
#   - "as:" renames loop.item to a custom name
#   - Loop outputs are collected into an array
#
# Try: orchstep run

name: basic-loop-demo
desc: "Iterate over lists, counts, and ranges"

defaults:
  servers:
    - "web-01.example.com"
    - "web-02.example.com"
    - "web-03.example.com"

tasks:
  main:
    desc: "Demonstrate loop patterns"
    steps:
      # Pattern 1: Loop over a list variable
      - name: check_servers
        desc: "Health-check each server"
        loop: "{{ vars.servers }}"
        func: shell
        do: |
          echo "Checking server {{ loop.index1 }}/{{ loop.length }}: {{ loop.item }}"
          echo "  First: {{ loop.first }}, Last: {{ loop.last }}"
        outputs:
          server: "{{ loop.item }}"
          position: "{{ loop.index1 }}"

      # Pattern 2: Loop a fixed number of times
      - name: retry_attempts
        desc: "Simulate 5 retry attempts"
        loop: 5
        func: shell
        do: echo "Attempt {{ loop.item }} of {{ loop.length }}"
        outputs:
          attempt: "{{ loop.item }}"

      # Pattern 3: Loop over a numeric range
      - name: port_scan
        desc: "Check ports 8080-8084"
        loop:
          range: [8080, 8084]
          as: port
        func: shell
        do: echo "Scanning port {{ loop.port }}"
        outputs:
          port: "{{ loop.port }}"

      # Pattern 4: Loop with a custom variable name
      - name: deploy_servers
        desc: "Deploy to each server using a named loop variable"
        loop:
          items: "{{ vars.servers }}"
          as: hostname
        func: shell
        do: |
          echo "Deploying to {{ loop.hostname }} ({{ loop.index1 }}/{{ loop.length }})"
        outputs:
          hostname: "{{ loop.hostname }}"

loop until

Loop Until

# Example: Loop Until
# The "until:" keyword breaks the loop when a condition becomes true.
# This is useful for polling, retrying, or searching through items.
#
# Key concepts:
#   - "until:" evaluates after each iteration
#   - When the until condition is true, the loop stops
#   - Supports Go template expressions: '{{ eq .loop.iteration 5 }}'
#   - Supports JavaScript expressions: 'loop.iteration === 5'
#   - Can reference step outputs for status-based breaking
#   - "delay:" adds a pause between iterations (useful for polling)
#
# Try: orchstep run find_target
# Try: orchstep run retry_until_ready
# Try: orchstep run search_items

name: loop-until-demo
desc: "Break loops with until conditions"

defaults:
  target_number: 5
  search_items:
    - name: "item-a"
      valid: false
    - name: "item-b"
      valid: false
    - name: "item-c"
      valid: true
    - name: "item-d"
      valid: false

tasks:
  # Count-based loop that stops at a target
  find_target:
    desc: "Loop until we reach the target number"
    steps:
      - name: count_to_target
        loop:
          count: 20
          until: '{{ eq .loop.iteration .vars.target_number }}'
        func: shell
        do: |
          echo "Checking: {{ loop.iteration }}"
        outputs:
          checked: "{{ loop.iteration }}"

  # Retry pattern with delay
  retry_until_ready:
    desc: "Poll a service until it becomes ready"
    steps:
      - name: wait_for_ready
        loop:
          count: 10
          until: '{{ eq .steps.wait_for_ready.status "ready" }}'
          delay: "100ms"
        func: shell
        do: |
          echo "Attempt {{ loop.iteration }}..."
          if [ {{ loop.iteration }} -ge 3 ]; then
            echo "STATUS: ready"
          else
            echo "STATUS: not_ready"
          fi
        outputs:
          attempt: "{{ loop.iteration }}"
          status: '{{ if ge loop.iteration 3 }}ready{{ else }}not_ready{{ end }}'

  # Search through items until a match is found
  search_items:
    desc: "Search a list until a valid item is found"
    steps:
      - name: find_valid
        loop:
          items: "{{ vars.search_items }}"
          as: item
          until: '{{ eq .loop.item.valid true }}'
        func: shell
        do: |
          echo "Checking: {{ loop.item.name }} (valid={{ loop.item.valid }})"
        outputs:
          item_name: "{{ loop.item.name }}"
          is_valid: "{{ loop.item.valid }}"

  # JavaScript-style until condition
  js_until_example:
    desc: "Loop until a JavaScript condition is met"
    steps:
      - name: count_up
        loop:
          count: 10
          until: 'loop.iteration > 5'
        func: shell
        do: echo "Iteration {{ loop.iteration }}"
        outputs:
          iteration: "{{ loop.iteration }}"

loop with conditions

Loop with Conditions

# Example: Loop with Conditions
# Combine loops with "if:" to filter which iterations execute.
# OrchStep evaluates the condition differently depending on context:
#
#   - if WITHOUT loop.* references -> evaluated ONCE before the loop
#     (acts as a gate: run the entire loop or skip it)
#   - if WITH loop.* references -> evaluated PER ITERATION
#     (acts as a filter: skip individual items)
#
# Try: orchstep run
# Try: orchstep run --var enable_deployment=false

name: loop-with-conditions-demo
desc: "Filter loop iterations using if conditions"

defaults:
  servers:
    - name: "web-01"
      type: "web"
      enabled: true
      port: 8080
    - name: "web-02"
      type: "web"
      enabled: false
      port: 8081
    - name: "db-01"
      type: "database"
      enabled: true
      port: 5432
    - name: "cache-01"
      type: "cache"
      enabled: true
      port: 6379
  enable_deployment: true

tasks:
  main:
    desc: "Deploy infrastructure with conditional filtering"
    steps:
      # Gate pattern: if without loop.* -> evaluated once
      # If enable_deployment is false, the entire loop is skipped
      - name: deploy_all
        loop: '{{ vars.servers }}'
        if: '{{ vars.enable_deployment }}'
        func: shell
        do: echo "Deploying {{ loop.item.name }} ({{ loop.item.type }})"

      # Filter pattern: if with loop.item -> evaluated per iteration
      # Only processes servers where enabled=true
      - name: deploy_enabled_only
        loop: '{{ vars.servers }}'
        if: '{{ loop.item.enabled }}'
        func: shell
        do: 'echo "Deploying enabled server: {{ loop.item.name }} on port {{ loop.item.port }}"'

      # Complex filter: multiple conditions
      # Only web servers that are enabled
      - name: deploy_web_servers
        loop: '{{ vars.servers }}'
        if: '{{ and (eq loop.item.type "web") loop.item.enabled }}'
        func: shell
        do: 'echo "Web server: {{ loop.item.name }} on port {{ loop.item.port }}"'

      # Filter by type
      - name: setup_databases
        loop: '{{ vars.servers }}'
        if: '{{ eq loop.item.type "database" }}'
        func: shell
        do: 'echo "Setting up database: {{ loop.item.name }} on port {{ loop.item.port }}"'

      # Show iteration metadata with filtering
      - name: enabled_server_report
        loop: '{{ vars.servers }}'
        if: '{{ loop.item.enabled }}'
        func: shell
        do: |
          echo "Server {{ loop.index1 }}/{{ loop.length }}: {{ loop.item.name }}"
          echo "  Type: {{ loop.item.type }}, Port: {{ loop.item.port }}"

loop with task call

Loop with Task Call

# Example: Loop with Task Call
# Loops can call tasks using "task:" and pass per-iteration parameters
# with "with:". This is ideal for deploying to multiple regions, running
# the same operation across environments, etc.
#
# Key concepts:
#   - "loop:" + "task:" calls the task once per iteration
#   - "with:" passes loop data as task parameters
#   - Loop variables (loop.item, loop.index, etc.) are available in "with:"
#   - "as:" renames loop.item for clearer references
#
# Try: orchstep run

name: loop-with-task-call-demo
desc: "Call tasks in a loop with per-iteration parameters"

defaults:
  regions:
    - name: "us-east-1"
      replicas: 3
      host: "east.example.com"
    - name: "us-west-2"
      replicas: 5
      host: "west.example.com"
    - name: "eu-west-1"
      replicas: 7
      host: "eu.example.com"

tasks:
  main:
    desc: "Deploy to all regions"
    steps:
      # Loop through regions, calling deploy_region for each
      - name: deploy_all_regions
        desc: "Deploy to every region using a reusable task"
        loop:
          items: "{{ vars.regions }}"
          as: region
        task: deploy_region
        with:
          region_name: "{{ loop.region.name }}"
          replica_count: "{{ loop.region.replicas }}"
          host_name: "{{ loop.region.host }}"
          progress: "{{ loop.index1 }}/{{ loop.length }}"

  # Reusable task called once per region
  deploy_region:
    desc: "Deploy to a single region"
    steps:
      - name: show_config
        func: shell
        do: |
          echo "=== Deploying Region [{{ vars.progress }}] ==="
          echo "Region:   {{ vars.region_name }}"
          echo "Host:     {{ vars.host_name }}"
          echo "Replicas: {{ vars.replica_count }}"
        outputs:
          region: "{{ vars.region_name }}"
          replicas: "{{ vars.replica_count }}"

      - name: execute_deploy
        func: shell
        do: |
          echo "kubectl apply -f deploy.yml \
            --context={{ vars.region_name }} \
            --replicas={{ vars.replica_count }}"
          echo "Status: deployed"
        outputs:
          status: "deployed"
          host: "{{ vars.host_name }}"