> 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 bounded loops

> Repeat a fixed sequence of Flow steps until a verdict passes, with a hard cap of 1 to 10 rounds.

Use a `loop` step when a fixed sequence of steps needs to repeat against shared Flow variables until a verdict passes. Common examples are draft, review, and revise; generate, validate, and repair; and retrieve, assess, and refine.

> **Note**
>
> The dashboard Flow editor doesn't have a loop step. Author bounded loops through the API, the
> Runtype MCP server, or the TypeScript SDK.

## How a loop runs

A bounded loop runs its body once, then evaluates `until`. It repeats the body only while the expression is false. `maxIterations` is a hard backstop: the loop stops after that many rounds even when the verdict never passes.

A loop step takes these fields in its `config`:

* `steps` (required): one or more step objects that form the body. Body steps use the same shape as top-level Flow steps.
* `until` (required): a JavaScript expression that ends the loop when it's true. It uses the same syntax as a conditional step's condition. For more information, see [Write a condition](/user-guide/flows/using-conditional-steps#write-a-condition).
* `maxIterations` (required): an integer from 1 to 10.
* `iterationVariable` (optional): a variable name that receives the current round number, starting at 1. The name can't start with `_`, because that prefix is reserved for system variables.

A loop step has no `outputVariable` and produces no result value of its own. After the loop, read the variables that the body wrote. The standard step-level `when` and `enabled` settings work on a loop step the same way they work on any other step. A falsy `when` skips the whole loop. For more information, see [Skip a single step with when](/user-guide/flows/using-conditional-steps#skip-a-single-step-with-when).

## Add a loop step

This loop reviews a draft and revises it until the review passes, for at most three rounds:

```json
{
  "type": "loop",
  "name": "Review and revise",
  "config": {
    "steps": [
      {
        "type": "prompt",
        "name": "Independent review",
        "config": {
          "model": "claude-haiku-4-5",
          "userPrompt": "Review {{draft}}. Return JSON with verdict and directives.",
          "responseFormat": "json",
          "outputVariable": "review"
        }
      },
      {
        "type": "prompt",
        "name": "Surgical revision",
        "config": {
          "model": "claude-sonnet-5",
          "userPrompt": "Revise {{draft}} using {{review.directives}}.",
          "outputVariable": "draft"
        }
      }
    ],
    "until": "review.verdict === 'pass'",
    "maxIterations": 3,
    "iterationVariable": "reviewRound"
  }
}
```

The body reads and writes the same Flow-level variables on every round. In this example, each revision replaces `draft`, and the next review reads the revised value. Because `iterationVariable` is set, Runtype writes the current round to `reviewRound` before each round's body runs.

MCP flow tools accept the same `type: "loop"` step shape in inline, create, and update requests.

## Build a loop with the TypeScript SDK

`FlowBuilder` and the `client.flows` builder both provide a `.loop()` method. Its `steps` field takes the same step objects as the JSON `config.steps` array:

```typescript
import { FlowBuilder } from '@runtypelabs/sdk'

const reviewAndReviseSteps = [
  {
    type: 'prompt',
    name: 'Independent review',
    config: {
      model: 'claude-haiku-4-5',
      userPrompt: 'Review {{draft}}. Return JSON with verdict and directives.',
      responseFormat: 'json',
      outputVariable: 'review',
    },
  },
  {
    type: 'prompt',
    name: 'Surgical revision',
    config: {
      model: 'claude-sonnet-5',
      userPrompt: 'Revise {{draft}} using {{review.directives}}.',
      outputVariable: 'draft',
    },
  },
]

const flow = new FlowBuilder()
  .createFlow({ name: 'Bounded review' })
  .loop({
    name: 'Review and revise',
    steps: reviewAndReviseSteps,
    until: "review.verdict === 'pass'",
    maxIterations: 3,
    iterationVariable: 'reviewRound',
  })
  .build()
```

## Check your loop

Validate the Flow before you run it. Call the validate endpoint (`POST /v1/public/flows/validate`) or the `validate_flow` MCP tool. Validation reports missing or out-of-range loop fields, such as a `maxIterations` above 10, and these errors for loop bodies:

* `NESTED_LOOP_UNSUPPORTED`: a loop body contains another `loop` step.
* `DUPLICATE_STEP_ID`: a body step reuses an id. Step ids must be unique across the whole Flow, including steps inside loop bodies.

For the full list of checks, see [Flow validation warnings](/user-guide/flows/flow-validation-warnings).

In a run's step results, the loop appears as a container step with the loop's name. Each round after the first runs inside a step named after the loop and the previous round. For example, a loop named `Review and revise` that runs two rounds shows round 1's steps, then a `Review and revise: continue after iteration 1` step that holds round 2's steps. When `iterationVariable` is set, each round also starts with a step named `Review and revise: iteration N`, where `N` is the round number. For more information about reading step results, see [Debugging flows](/user-guide/flows/debugging-flows).

## Design safe loop bodies

Follow these guidelines when you design a loop body:

* Make every round move one shared working variable toward a measurable verdict. Avoid redrafting from the original input on every round.
* Keep `until` deterministic. Have a review or validation step write a boolean or compact verdict, then test that value.
* Choose the smallest useful bound. Each round runs every body step again, so the worst case is `maxIterations` times the body's model calls, cost, and time. Consider a faster, cheaper model for the review step.
* Handle exhaustion explicitly. Reaching `maxIterations` completes the loop and continues the Flow, but it doesn't mean the verdict passed. Add a conditional step after the loop with a condition such as `review.verdict !== 'pass'` to route that case.
* Plan for failed body steps. By default, a failed body step doesn't stop the round, and the next body step runs. A `tool-call` step is the exception: it stops the loop unless its error handling is set to continue. Write `until` so it stays false when the review step fails and its verdict variable doesn't update. When a body step sets `errorHandling.onError` to `fail`, a failure in that step stops the rest of the loop and the loop step reports a failure.
* Don't nest `loop` steps. Put the inner work in the same body, call a separate Flow, or use a conditional step inside the body.

## Next steps

* [Flow variables and templates](/user-guide/flows/flow-variables-and-templates): share working context between rounds
* [Using conditional steps](/user-guide/flows/using-conditional-steps): route based on the final verdict
* [Debugging flows](/user-guide/flows/debugging-flows): inspect each round's step results
* [Timeouts and long-running work](/user-guide/flows/timeouts-and-long-running-work): use asynchronous handles for runs that may outlive a client request