Steps
The waitFor step
A waitFor step polls an HTTP endpoint until a condition holds, then lets the task go on. It is
for a service that is still coming up, a job that has not finished, a cache that has not filled.
It asserts nothing itself: once the condition is true the next step does the checking, with a
single request vero can count and time.
| Key | Type | Meaning |
|---|---|---|
http |
object | the request to poll, as an http step |
until |
expression | the readiness condition, http environment |
timeout |
duration | required |
interval |
duration | default 500 ms |
A complete plan
wait-for-api.yaml waits for the fixture server's health
endpoint and then checks it properly in a second task:
tasks:
- name: wait-for-api
steps:
- waitFor:
http: { method: GET, url: "{{ env.baseUrl }}/healthz" }
until: "status == 200"
timeout: 30s
interval: 250ms
- name: api-answers
needs: [wait-for-api]
steps:
- http: { method: GET, url: "{{ env.baseUrl }}/healthz" }
assert:
- status == 200
- headers["Content-Type"] startsWith "application/json"
- duration.ttfb < 1s
$ bin/vero run examples/wait-for-api.yaml
run 01M3ZRX3Y0YB24F735TDHWGART wait-for-api.yaml --jobs 4 lifecycle isolated
waitFor wait-for-api › step 1 passed after 1 attempts, waited 2.6ms
2 passed 0 failed 0 errored 0 timed out 0 skipped 0 blocked 0 not run (2 tasks, 2 requests, 0.0s)
Every wait gets a line in the summary block with its attempts and the time it waited. Each attempt is a request and counts in the request total.
How it polls
Each attempt is a full request built from the http block, templates rendered, the way an
http step sends it. The attempt runs under the wait's whole timeout as its own
deadline, through the task's cookie jar when the task has cookies: true, and with the block's
followRedirects if set. A connection error is "not yet". The condition is evaluated only when a
response arrived: until is compiled in the http environment, so it may read status,
headers, body and duration like any http assertion. When it holds, the step is passed and
the task moves on. When it does not, vero waits interval and tries again.
Running out of timeout makes the step timed_out, which is exit 2
(states and exit codes). The reason says how many attempts
were made and what the last one saw:
TIMEOUT wait-for-api › step 1 wait-503.yaml
waitFor did not see "status == 200" within 1s: 4 attempts, last status 503
GET http://127.0.0.1:18080/fixtures/ready?after=100 HTTP/1.1 503 Service Unavailable 0.5ms
> accept-encoding: gzip
> user-agent: vero
< content-length: 21
< content-type: application/json
< date: Sat, 03 Oct 2026 02:18:49 GMT
< {"error":"starting"}
timing dns n/a connect n/a tls n/a ttfb 0.4ms total 0.5ms (reused connection)
waitFor wait-for-api › step 1 timed out after 4 attempts, waited 1002.3ms
0 passed 0 failed 0 errored 1 timed out 0 skipped 0 blocked 0 not run (1 tasks, 4 requests, 1.0s)
Against a port nothing listens on, the reason carries the error instead:
waitFor did not see "status == 200" within 1s: 4 attempts, last error: Get "http://127.0.0.1:1/healthz": dial tcp 127.0.0.1:1: connect: connection refused
When the wait's timeout cuts an attempt off mid-flight after earlier attempts did answer, the
reason keeps the status the service had been returning, as in last status 503; the timeout cut off attempt 5 waiting for first byte. A wait where no attempt finished says no attempt finished.
The event log gets one attempt event per request, with its status, whether the condition held,
and its duration (vero.events/v1).
What the wait costs
vero plan counts a wait's requests as an upper bound. With timeout: 30s and
interval: 250ms, the bound is 121 attempts, which is the timeout divided by the interval,
rounded up, plus one:
$ bin/vero plan examples/wait-for-api.yaml
ok: 2 tasks, 0 finally tasks; the plan sends 1 HTTP request, plus up to 121 from waitFor
vero plan --explain wait-for-api describes the step as
waitFor GET {{ env.baseUrl }}/healthz until status == 200: up to 121 attempts (timeout 30s / interval).
What a waitFor step may not do
A wait repeats until it is ready, so it cannot take part in counting anything, and it cannot sit inside a claim on a shared resource, where every request must be one the plan wrote. The loader refuses each of these, and reports them all at once:
| Plan | Load error |
|---|---|
no http |
waitFor needs an http block to poll |
no until |
waitFor needs until, the condition that means ready |
no timeout |
waitFor needs a timeout; waiting has no default, because a wait with no end is a hung run |
assert on the step |
a waitFor step may not carry assert: it repeats until ready, so it cannot take part in counting anything (chapter 7.3); put the assertions in the next step |
repeat or results on the step |
a waitFor step takes neither repeat nor results |
timeout on the step itself |
a waitFor step's deadline is waitFor.timeout; a second timeout beside it would only disagree with it |
retry inside http |
waitFor already repeats until ready; retry inside it has nothing to add |
in a task with holds |
task wait-for-api holds budget, so it may not contain a waitFor step; wait in a task of its own that holds nothing |
The step-level timeout every other step kind takes is refused here: the wait's own timeout is
its deadline, and there is no 30 s default to fall back on. Put the wait in a task of its own, as
the example does, and let the tasks that hold resources needs it.
A waitFor step has no assertions, so there are no names in scope for it; until sees the http
names. It publishes no results. The task's next step sees nothing of the wait but the cookies it
collected.
This page is docs/content/steps/wait-for.md in the repository.