Skip to navigation

Using bounded loops

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.

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.
  • 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.

Add a loop step

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

{
"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:

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.

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.

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