> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runtype.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runtype.com/_mcp/server.

# Using Wait Until steps

> Pause a Flow for a fixed delay, or poll an HTTP endpoint until a success condition passes.

Use a **Wait Until** step when a Flow must wait before it continues. The step does one or both of these things:

* Waits for a fixed delay.
* Polls an HTTP endpoint until the response meets a success condition, such as a job status of `done`.

When you set both, the step waits for the delay first, then starts polling.

## Add a Wait Until step in the dashboard

To add the step, follow these steps:

1. In the Flow editor, click the **+** control where you want to insert the step.
2. In the step picker, under **Actions**, select **Wait Until**.
3. In **Wait**, enter the delay in milliseconds. Leave it empty to start polling right away.
4. To poll an endpoint, turn on **Enable HTTP polling**, then configure the request:
   1. Choose the HTTP method and enter the URL. The URL accepts Flow variables, such as `{{jobId}}`, and secret references, such as `{{secret:API_KEY}}`.
   2. In **every**, enter the time between attempts in milliseconds. The minimum is 100.
   3. In **up to**, enter the maximum number of attempts.
   4. Under **Success when**, choose a condition. For the options, see [Choose a success condition](#choose-a-success-condition).
   5. Optional: under **Headers**, add request headers. Under **Advanced**, set the **response type** and the **timeout (ms)** for each request.
5. Set the output variable. A new step uses `wait_result`.
6. Choose the step's error handling mode. A new step starts with **Stop on error**. For what each mode does, see [Handle a poll that never succeeds](#handle-a-poll-that-never-succeeds).

## Add a Wait Until step to a Flow definition

A `wait-until` step takes these fields in its `config`:

* `delayMs`: the delay before polling, in milliseconds.
* `poll`: the polling configuration. Set `delayMs`, `poll`, or both.
* `outputVariable` (required): the variable that receives the step's result.
* `continueOnTimeout`: when `true`, the Flow continues after a poll that never succeeds. The default is `false`.
* `defaultValue`: the value that the output variable receives when the poll never succeeds and the Flow continues.
* `errorHandling`: the error handling mode, such as `{ "onError": "continue" }`.

The `poll` object takes these fields:

* `http` (required): the request. It takes `url`, `method` (`GET`, `POST`, `PUT`, `PATCH`, or `DELETE`, default `GET`), `headers`, `body`, and `timeout` in milliseconds. When `timeout` is unset, each request can take up to 2 minutes.
* `intervalMs`: the time between attempts. The default is 2000 and the minimum is 100.
* `maxAttempts`: the maximum number of requests. The default is 10.
* `responseType`: `json` (default) or `text`. With `json`, Runtype parses the response body so that `json_path` and expression conditions can read its fields.
* `success`: the success condition. When it's unset, any 2xx response succeeds.

The following step starts an export job's poll after a 5-second delay. It checks the job every 30 seconds, up to 20 times, until the job reports `done`:

```json
{
  "type": "wait-until",
  "name": "Wait for export job",
  "config": {
    "delayMs": 5000,
    "poll": {
      "http": {
        "url": "https://api.example.com/jobs/{{jobId}}",
        "method": "GET",
        "headers": {
          "Authorization": "Bearer {{secret:EXPORT_API_KEY}}"
        }
      },
      "intervalMs": 30000,
      "maxAttempts": 20,
      "responseType": "json",
      "success": {
        "type": "json_path",
        "jsonPath": "$.status",
        "operator": "=",
        "expectedValue": "done"
      }
    },
    "outputVariable": "jobResult"
  }
}
```

In this example, `jobId` is a Flow variable that an earlier step sets, and `EXPORT_API_KEY` is a secret in your organization. For more information about secrets, see [Managing secrets](/user-guide/settings/managing-secrets).

## Choose a success condition

Set `success.type` to one of the following conditions. The dashboard label for each condition is in parentheses.

| `type`          | Dashboard option             | Fields                                  | Succeeds when                                           |
| --------------- | ---------------------------- | --------------------------------------- | ------------------------------------------------------- |
| `status`        | **HTTP status code matches** | `status`                                | The response status equals `status`, or is 2xx if unset |
| `json_path`     | **JSON path equals value**   | `jsonPath`, `operator`, `expectedValue` | The value at `jsonPath` passes the comparison           |
| `body_contains` | **Body contains text**       | `bodyContains`                          | The raw response body contains the text                 |
| `expression`    | **Custom expression**        | `expression`                            | The JavaScript expression returns a truthy value        |

A request that fails, such as a network error or a request timeout, counts as an unsuccessful attempt, and the poll continues.

For a `json_path` condition, write the path in dot notation, such as `data.state` or `items[0].name`. A leading `$.` is optional. The `operator` field accepts `=`, `!=`, `>`, `<`, `>=`, `<=`, `exists`, and `not-exists`. Without an `operator`, the condition passes when the value equals `expectedValue`, or when the value exists if `expectedValue` is unset. Runtype converts an `expectedValue` string such as `"1"`, `"true"`, or `"null"` to a number, boolean, or `null` before it compares.

An `expression` condition can read these values:

* `status`, `ok`, `data`, `text`, and `headers` from the most recent response. `data` is the parsed JSON body.
* `attempt`: the current attempt number, starting at 1.
* `flowContext`: the Flow's variables.

For example, the expression `status === 200 && data.state === 'done'` succeeds when the endpoint returns `200` and a `state` of `done`.

## Read the result

When the step finishes, the output variable holds an object with these fields:

* `completed`: `true` when the success condition passed, or when the step had no poll.
* `attempts`: the number of requests that the step made.
* `totalDelayMs`: the time that the step spent waiting and polling.
* `lastStatus`, `lastResponse`, `lastResponseText`, and `lastHeaders`: the status, parsed body, raw body, and headers of the last response.
* `timedOut`: `true` when the poll used every attempt without success.
* `error`: the last request error, if any.

Later steps can read the response from the last attempt. For example, in the export job example, `{{jobResult.lastResponse.downloadUrl}}` reads a `downloadUrl` field from the job's final status response.

## Handle a poll that never succeeds

If the poll uses all its attempts without meeting the success condition, the step's error handling decides what happens:

* With **Stop on error** (`"onError": "fail"`), the step fails and the Flow stops. A step in a Flow definition that has no `errorHandling` field also stops the Flow.
* With **Continue on error** (`"onError": "continue"`) or `continueOnTimeout: true`, the Flow continues. The output variable holds `defaultValue`, or `null` if you didn't set one. It doesn't hold the result object.

When the Flow continues, add a **Conditional Logic** step after the wait to check the output before later steps use it. For more information, see [Using conditional steps](/user-guide/flows/using-conditional-steps).

## Plan for long waits

A poll that uses a `status`, `json_path`, or `body_contains` condition pauses the Flow between attempts. The pause doesn't count toward the Flow's timeouts, so a poll can wait for hours or days.

These parts of the step keep the step running instead of pausing the Flow, so they count toward the step timeout:

* The `delayMs` delay.
* A poll with an `expression` condition.
* A poll with an `auth` object, or with a secret reference in the request `body`. To send credentials in a poll that pauses, put the secret reference in a header, as the export job example does.

To wait longer than the step timeout, use a poll with a `status`, `json_path`, or `body_contains` condition, and set a longer `intervalMs`.

A pausing poll has these limits:

* It runs at most 1,000 attempts. Runtype lowers a larger `maxAttempts` value to 1,000, and Flow validation reports the `WAIT_UNTIL_MAX_ATTEMPTS_CLAMPED` warning.
* Its worst-case duration must be 30 days minus one hour or less. The worst case is `maxAttempts` times each request's timeout, plus `intervalMs` for each wait between attempts. A longer poll fails validation with `WAIT_UNTIL_POLL_EXCEEDS_RESUME_TTL`.

A Flow that contains a Wait Until step runs through interactive dispatch. Batch runs, evals, and Schedules refuse the Flow, and an Agent's inline Flow tools can't contain the step. For the entry points that support pauses, see [Timeouts and long-running work](/user-guide/flows/timeouts-and-long-running-work#wait-for-slow-external-work-with-durable-pauses).

## Next steps

* [Timeouts and long-running work](/user-guide/flows/timeouts-and-long-running-work): choose timeouts and run a paused Flow asynchronously
* [Flow validation warnings](/user-guide/flows/flow-validation-warnings): fix the validation codes for Wait Until steps
* [Using fetch-url and api-call steps](/user-guide/flows/using-fetch-url-and-api-call-steps): start the job that the Wait Until step polls
* [Flow variables and templates](/user-guide/flows/flow-variables-and-templates): reference the wait result in later steps