Recipe: Loop (Iterate + Terminate)
NOTE
Goal: Iterate safely and explicitly, with a visible termination reason.
Loop is a small orchestration recipe that focuses on one idea: repeat a step a fixed number of times, then decide how to finish. It models the pattern “do something N times, then finalise” and keeps control flow visible in your own code and in the explain view.
Loop works well when you want clear, auditable behaviour. It gives you a control flow skeleton rather than a full framework. You can swap models, wire in adapters, and connect it to larger recipes while the public surface stays the same.
flowchart LR Seed[Seed input] --> Iterate[Iterate] Iterate --> Finalize[Finalize] Finalize --> Output([Outcome])
1) Quick start: input and max iterations
The input stays intentionally small. You pass in:
- a seed string (for example a question or instruction)
- an optional maximum iteration count
Each run returns the standard outcome shape:
statusartefactdiagnosticstrace
When the outcome is ok, the artefact includes a loop section. This section exposes loop.iterations, loop.result, and loop.terminationReason. With these fields you can see how many steps ran, what the final value is, and why the loop ended.
import { recipes } from "@geekist/llm-core/recipes";
import { fromAiSdkModel } from "@geekist/llm-core/adapters";
import { openai } from "@ai-sdk/openai";
const loop = recipes.loop().defaults({
adapters: { model: fromAiSdkModel(openai("gpt-4o-mini")) },
});
const input = {
input: "Summarize this in three steps.",
maxIterations: 3,
};
const outcome = await loop.run(input);For more detail about how outcomes behave across recipes, see Runtime Outcomes and Recipes API.
2) Configure defaults (typed)
Loop has a compact config surface. It focuses on control flow and delegates most model specific behaviour to adapters.
Use configure() to set recipe scoped defaults such as the system prompt or general loop policy. Use defaults() to wire in adapters and runtime options. The config is recipe specific and typed, which gives you editor support while you tune behaviour.
Typical tweaks include:
- setting a model that is friendly to short, iterative turns
- defining a system prompt that explains how each step should refine the result
- choosing a reasonable
maxIterationsfor your use case
import { recipes } from "@geekist/llm-core/recipes";
import { fromAiSdkModel } from "@geekist/llm-core/adapters";
import { openai } from "@ai-sdk/openai";
const loop = recipes.loop().configure({
defaults: {
adapters: { model: fromAiSdkModel(openai("gpt-4o-mini")) },
},
});3) Diagnostics and trace
Loop runs with the same diagnostics and trace guarantees as other recipes. Every iteration can contribute entries to the trace, and validation or adapter issues appear in the diagnostics array.
This gives you a clear view of the loop lifecycle. You can see when each step started, which adapters ran, and where a problem occurred if the loop stops earlier than expected. Strict diagnostics mode helps when you treat missing adapters or invalid inputs as hard failures during development and testing.
import { recipes } from "@geekist/llm-core/recipes";
import { fromAiSdkModel } from "@geekist/llm-core/adapters";
import { openai } from "@ai-sdk/openai";
const loop = recipes.loop().defaults({
adapters: { model: fromAiSdkModel(openai("gpt-4o-mini")) },
});
const outcome = await loop.run(
{ input: "Iterate a plan.", maxIterations: 2 },
{ runtime: { diagnostics: "strict" } },
);
console.log(outcome.diagnostics, outcome.trace);You can read more about this behaviour in Runtime: Diagnostics and Runtime: Trace.
4) Composition and explain
Loop is a control flow building block. You can place it on its own when you need a simple iterative refinement, or you can compose it with other recipes such as RAG or Agent.
The explain API keeps the execution model transparent. A Loop plan shows the seed, the iterate step, and the finalisation step as named nodes. This makes it easy to describe behaviour to team members and to reason about changes over time.
flowchart LR Seed[loop.seed] --> Iterate[loop.iterate] Iterate --> Finalize[loop.finalize]
import { recipes } from "@geekist/llm-core/recipes";
const workflow = recipes.loop().use(recipes.hitl());
const plan = workflow.explain();You can start from a single loop recipe and later plug it into a larger workflow. The composition surface remains predictable because the recipe treats iteration as an explicit concern rather than a hidden side effect inside user code.
5) Why Loop is special
Loop promotes iteration to a first class concept. It provides explicit outputs and termination metadata, which keeps runtime behaviour clear even when models, prompts, or adapters change.
This design suits several patterns:
- iterative reasoning where each step refines a draft
- gradual summarisation of long content over a fixed number of passes
- small batch experiments where you want a fixed budget of attempts
In each case you gain a stable structure: a visible count of iterations, a final result, and a stated termination reason.
Implementation
Source: src/recipes/loop/index.ts