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.

verodocs