> ## Documentation Index
> Fetch the complete documentation index at: https://smithers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# <Loop>

> Re-run children until `until` is true or `maxIterations` is hit.

`<Loop until={done} maxIterations={5}>…</Loop>` re-runs its children each frame until `until` evaluates true or `maxIterations` is reached.

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
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 theme={"theme":{"light":"github-light","dark":"github-dark"}}
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 drives **compute** tasks (an
`async` function body, no agent) as well as agent tasks. Canonical
non-agent case: a retry-until-green test loop.

```tsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
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`. An auto id derives from tree position, so a conditionally mounted/unmounted sibling shifts the path, re-keying the loop and resetting its iteration state from 0; an explicit `id` is immune.
* `onMaxReached` has two values: `"return-last"` (default) ends the loop cleanly at `maxIterations`, keeping the last output; `"fail"` throws `RALPH_MAX_REACHED` and fails the run. Use `"fail"` when hitting the cap means the work never converged (tests never went green), 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` remains a deprecated alias of `Loop`; new code should use `Loop`.
