While Loops & Waiting
OrchStep has no unbounded while loop by design. Here's how to express every "repeat until" intent safely - poll until ready with func wait, retry until success, and loop ... until for early-exit iteration.
OrchStep has no unbounded while loop - on purpose. Loops always iterate a
finite set (items / count / range), and "keep going until X" is expressed
with bounded, time-boxed primitives. A workflow that runs the same in CI should
never be able to hang forever, so every "while" you'd reach for maps to a
primitive with a built-in stop.
This page shows how to do each one.
Which one do I want?
| You want to... | Use | Bounded by |
|---|---|---|
| Wait until a condition is true (poll something) | func: wait with condition | timeout |
| Keep retrying an action until it succeeds | retry: on the step | max_attempts |
| Iterate a known list but stop as soon as a condition holds | loop: + until: | the list (items/count/range) |
| Repeat a check until ready, never forever | func: wait, or loop: { count, until } | timeout / count |
Wait until a condition is true (polling)
The closest thing to while not ready: sleep is func: wait in condition
mode. It polls a condition on an interval and fails the step at timeout,
so it can't hang.
- name: deploy
func: shell
do: kubectl rollout restart deploy/api -n {{ vars.namespace }}
- name: wait_until_healthy
func: wait
args:
condition: '{{ eq steps.health.status_code 200 }}'
timeout: "5m" # give up (and fail) after 5 minutes
interval: "10s" # check every 10 secondsWhy this instead of a while loop: it reads as intent ("wait until healthy"),
polls on a schedule, and is guaranteed to terminate at the timeout. See the
wait function for the full parameter list.
Retry until it succeeds
"While this keeps failing, try again" is a retry:
block, not a loop. It re-runs the step with backoff until it passes or runs out
of attempts.
- name: smoke_test
func: shell
do: curl -fsS https://{{ vars.host }}/health
retry:
max_attempts: 5 # the hard stop
interval: 3s # initial delay
backoff_rate: 2 # 3s, 6s, 12s, ...
when: '!result.success' # keep retrying while it failsretry is the right tool whenever the "condition" is really "did this command
work?" - flaky networks, slow rollouts, eventual consistency.
Loop until a condition (early exit)
When you are iterating a known collection but want to bail out the moment a
condition is met, add until: to the loop. It still can't run longer than the
list, but it stops early.
defaults:
servers:
- { host: web1, status: pending }
- { host: web2, status: active }
- { host: web3, status: active }
tasks:
main:
steps:
- name: find_active
loop:
items: "{{ vars.servers }}"
as: server
until: '{{ eq loop.server.status "active" }}' # stop at the first active one
func: shell
do: echo "checking {{ loop.server.host }} ({{ loop.index1 }}/{{ loop.length }})"The loop runs web1, sees web2 is active, and breaks - it never touches
web3. The until expression sees the normal loop context (loop.item /
loop.<as>, loop.index1, loop.length, loop.first, loop.last).
"Bounded while": repeat up to N times until ready
For "keep doing this until ready, but never forever," reach for func: wait
(above) when you're polling external state. When each attempt must re-run a
command, use a count loop with until: and a delay: between iterations -
the count is your safety cap.
- name: poll_migration
loop:
count: 30 # never more than 30 tries
delay: 10s # wait between checks
until: '{{ contains "READY" result.output }}' # stop when ready
func: shell
do: ./scripts/migration-status.shThis is the bounded equivalent of while not ready: { check; sleep } - it polls
up to count times, delay apart, and exits as soon as until is satisfied.
Pagination & unknown counts
The one case where a true while feels missing is "loop while there's a next
page" - you don't know the page count up front. Express it as a generous
count cap plus an until break:
- name: drain_pages
loop:
count: 1000 # safety cap - you'll never hit it
until: '{{ eq result.data_object.next_cursor "" }}' # stop when there's no next page
func: shell
do: ./fetch-page.shThe count isn't your intent - it's a guardrail. Pick a number you're confident
exceeds any real run, and let until end the loop normally.
Why no unbounded while?
It's a deliberate omission, not a gap:
- It can't hang. OrchStep is built to run unattended in CI. An unbounded loop is a stuck runner, wasted minutes, and runaway cost. Every loop here has a hard stop.
- It matches the category. GitHub Actions has no loops at all; Ansible bounds
"until" with
retries; Airflow polls with sensor timeouts; Terraform isfor_eachonly. Unboundedwhileis the path these tools avoid. - It's safe for AI-authored workflows. When an agent captures or designs a workflow, a missing bound would be an easy way to ship an accidental infinite loop. Bounded-by- default keeps generated workflows safe and replay deterministic.
Where to go next
- Loops - iterate over lists, counts, and ranges.
- Error Handling -
retry,catch,finally,on_error. waitfunction - fixed delays and condition polling.