---
title: <Loop>
description: Re-run children until `until` is true or `maxIterations` is hit.
---

```ts
import { Loop } from "smithers-orchestrator";

type LoopProps = {
  key?: string;
  id?: string; // give an explicit id; auto-generated from tree position otherwise
  until?: boolean;
  maxIterations?: number; // default 5
  onMaxReached?: "fail" | "return-last"; // default "return-last"
  continueAsNewEvery?: number; // checkpoint every N iters to bound history
  skipIf?: boolean;
  children?: ReactNode;
};
```

```tsx
export default smithers((ctx) => {
  const latestReview = ctx.latest("review", "review");

  return (
    <Workflow name="refine-loop">
      <Loop until={latestReview?.approved === true} maxIterations={5}>
        <Sequence>
          <Task id="write" output={outputs.draft} agent={writer}>
            {latestReview
              ? `Improve the draft. Feedback: ${latestReview.feedback}`
              : `Write a draft about: ${ctx.input.topic}`}
          </Task>
          <Task id="review" output={outputs.review} agent={reviewer}>
            {`Review the latest draft.`}
          </Task>
        </Sequence>
      </Loop>
    </Workflow>
  );
});
```

Loop children are ordinary Tasks, so a loop can drive **compute** tasks (an
`async` function body, no agent) just as well as agents. A retry-until-green
test loop is the canonical non-agent case:

```tsx
export default smithers((ctx) => {
  const last = ctx.latest("testRun", "run-tests");

  return (
    <Workflow name="retry-tests">
      <Loop id="until-green" until={last?.passed === true} maxIterations={3} onMaxReached="fail">
        <Task id="run-tests" output={outputs.testRun}>
          {async () => {
            const proc = Bun.spawn(["bun", "test"], { stdout: "pipe" });
            const log = await new Response(proc.stdout).text();
            return { passed: (await proc.exited) === 0, log };
          }}
        </Task>
      </Loop>
    </Workflow>
  );
});
```

## Notes

- Give every `<Loop>` an explicit `id`. Without one the loop id is derived from its position in the tree, so a sibling that conditionally mounts or unmounts shifts the path and re-keys the loop, stranding its iteration state and restarting it from 0. An explicit `id` pins identity and makes the loop immune to sibling churn.
- `onMaxReached` has exactly two values. `"return-last"` (the default) ends the loop cleanly once `maxIterations` is hit, keeping the last iteration's output. `"fail"` instead throws `RALPH_MAX_REACHED` and fails the run. Use `"fail"` when hitting the cap means the work never converged (tests never went green); use the default when the best-so-far result is acceptable.
- `ctx.latest(table, nodeId)` reads the highest-iteration output; `until` must use `ctx.outputMaybe()` since output is absent on iter 0.
- Direct nesting of `<Loop>` in `<Loop>` throws; wrap the inner loop in `<Sequence>`.
- Custom Drizzle tables for loop tasks require `iteration` in the primary key.
- `Ralph` is still exported as a deprecated alias of `Loop` for older workflows. New code should import and render `Loop`.
