Skip to content

Iterative Refinement

AI-generated content rarely achieves production quality on the first attempt. Code needs debugging. Articles need editing. Designs need revision. This tutorial shows you how to use RepeatUntil for quality-driven iteration, where workflows repeat until a threshold is met or a maximum iteration count is reached.

A content refinement workflow that:

  1. Generates an initial draft on a topic
  2. Critiques the draft and assigns a quality score
  3. Refines the content based on critique feedback
  4. Repeats steps 2-3 until quality reaches 90% or 5 iterations complete
  5. Publishes the final content

The state tracks the current draft, quality score, iteration count, and history:

[WorkflowState]
public record RefinementState : IWorkflowState
{
public Guid WorkflowId { get; init; }
public string Topic { get; init; } = string.Empty;
public string? CurrentDraft { get; init; }
public decimal QualityScore { get; init; }
public int IterationCount { get; init; }
[Append]
public ImmutableList<CritiqueResult> CritiqueHistory { get; init; } = [];
[Append]
public ImmutableList<RefinementAttempt> RefinementHistory { get; init; } = [];
public string? FinalContent { get; init; }
}
public record CritiqueResult(
decimal Score,
IReadOnlyList<string> Strengths,
IReadOnlyList<string> Weaknesses,
IReadOnlyList<string> Suggestions);
public record RefinementAttempt(
int Iteration,
string BeforeContent,
string AfterContent,
IReadOnlyList<string> ChangesApplied);

The [Append] attribute on the history collections ensures each iteration’s results accumulate rather than overwrite.

Step 2: Define the Workflow with RepeatUntil

Section titled “Step 2: Define the Workflow with RepeatUntil”

The RepeatUntil method takes a condition, maximum iterations, and a loop body:

var workflow = Workflow<RefinementState>
.Create("iterative-refinement")
.StartWith<GenerateDraft>()
.RepeatUntil(
condition: state => state.QualityScore >= 0.9m,
maxIterations: 5,
body: flow => flow
.Then<Critique>()
.Then<Refine>())
.Finally<Publish>();

The loop continues until either:

  1. QualityScore >= 0.9 (quality threshold met)
  2. 5 iterations have been completed (maximum reached)

After exiting the loop, execution continues to Publish.

Stop when output quality is sufficient:

.RepeatUntil(
condition: state => state.QualityScore >= 0.9m,
maxIterations: 10,
body: flow => flow
.Then<Evaluate>()
.Then<Improve>())

Stop when improvement plateaus:

.RepeatUntil(
condition: state => state.ImprovementDelta < 0.01m,
maxIterations: 20,
body: flow => flow
.Then<Iterate>()
.Then<MeasureDelta>())

Stop when all errors are fixed:

.RepeatUntil(
condition: state => state.Errors.Count == 0,
maxIterations: 3,
body: flow => flow
.Then<Validate>()
.Then<FixErrors>())

Stop when tests pass:

.RepeatUntil(
condition: state => state.AllTestsPassing,
maxIterations: 5,
body: flow => flow
.Then<GenerateCode>()
.Then<RunTests>()
.Then<AnalyzeFailures>()
.Then<RefineCode>())

Creates the initial content:

public class GenerateDraft : IWorkflowStep<RefinementState>
{
private readonly IContentGenerator _generator;
public GenerateDraft(IContentGenerator generator)
{
_generator = generator;
}
public async Task<StepResult<RefinementState>> ExecuteAsync(
RefinementState state,
StepContext context,
CancellationToken ct)
{
var draft = await _generator.GenerateAsync(state.Topic, ct);
return state
.With(s => s.CurrentDraft, draft)
.With(s => s.IterationCount, 0)
.AsResult();
}
}

Evaluates the current draft and assigns a score:

public class Critique : IWorkflowStep<RefinementState>
{
private readonly IContentCritic _critic;
public Critique(IContentCritic critic)
{
_critic = critic;
}
public async Task<StepResult<RefinementState>> ExecuteAsync(
RefinementState state,
StepContext context,
CancellationToken ct)
{
var critique = await _critic.CritiqueAsync(state.CurrentDraft!, ct);
return state
.With(s => s.QualityScore, critique.Score)
.With(s => s.CritiqueHistory, state.CritiqueHistory.Add(critique))
.AsResult();
}
}

Improves the draft based on critique suggestions:

public class Refine : IWorkflowStep<RefinementState>
{
private readonly IContentRefiner _refiner;
public Refine(IContentRefiner refiner)
{
_refiner = refiner;
}
public async Task<StepResult<RefinementState>> ExecuteAsync(
RefinementState state,
StepContext context,
CancellationToken ct)
{
var latestCritique = state.CritiqueHistory[^1];
var refinedContent = await _refiner.RefineAsync(
state.CurrentDraft!,
latestCritique.Suggestions,
ct);
var attempt = new RefinementAttempt(
Iteration: state.IterationCount + 1,
BeforeContent: state.CurrentDraft!,
AfterContent: refinedContent,
ChangesApplied: latestCritique.Suggestions);
return state
.With(s => s.CurrentDraft, refinedContent)
.With(s => s.IterationCount, state.IterationCount + 1)
.With(s => s.RefinementHistory, state.RefinementHistory.Add(attempt))
.AsResult();
}
}

Outputs the final content:

public class Publish : IWorkflowStep<RefinementState>
{
private readonly IPublisher _publisher;
public Publish(IPublisher publisher)
{
_publisher = publisher;
}
public async Task<StepResult<RefinementState>> ExecuteAsync(
RefinementState state,
StepContext context,
CancellationToken ct)
{
await _publisher.PublishAsync(state.CurrentDraft!, ct);
return state
.With(s => s.FinalContent, state.CurrentDraft)
.AsResult();
}
}

Loop steps are prefixed with a loop identifier for uniqueness:

public enum IterativeRefinementPhase
{
NotStarted,
GenerateDraft,
Refinement_Critique, // Loop "Refinement" contains "Critique"
Refinement_Refine, // Loop "Refinement" contains "Refine"
Publish,
Completed,
Failed
}

The generated saga handles loop logic automatically:

// After Refine step completes
public async Task<object> Handle(
ExecuteRefinement_RefineCommand command,
Refine step,
CancellationToken ct)
{
var result = await step.ExecuteAsync(State, ct);
State = RefinementStateReducer.Reduce(State, result.StateUpdate);
// Check loop exit condition
if (State.QualityScore >= 0.9m)
{
// Exit loop - proceed to Publish
return new ExecutePublishCommand(WorkflowId);
}
if (State.IterationCount >= 5)
{
// Max iterations reached - proceed to Publish
return new ExecutePublishCommand(WorkflowId);
}
// Continue loop - back to Critique
return new ExecuteRefinement_CritiqueCommand(WorkflowId);
}

The [Append] attribute accumulates history across iterations:

// After 3 iterations, CritiqueHistory contains:
// [Critique1, Critique2, Critique3]
// RefinementHistory contains:
// [Attempt1, Attempt2, Attempt3]

This provides a complete audit trail of how content evolved through refinement.

Loops can be nested for complex refinement patterns:

.RepeatUntil(
condition: state => state.ChapterCount >= 5,
maxIterations: 10,
body: flow => flow
.Then<GenerateChapter>()
.RepeatUntil(
condition: state => state.ChapterQuality >= 0.8m,
maxIterations: 3,
body: inner => inner
.Then<CritiqueChapter>()
.Then<RefineChapter>()))

Phase names preserve the hierarchy:

public enum BookWritingPhase
{
Chapters_GenerateChapter,
Chapters_ChapterRefinement_CritiqueChapter,
Chapters_ChapterRefinement_RefineChapter,
// ...
}

Because each iteration is persisted, workflows can recover mid-loop:

  1. Iteration 1 - Critique and Refine complete, persisted
  2. Iteration 2 - Critique completes, process crashes
  3. On restart - Workflow resumes at Refine in iteration 2

No work is lost, and the iteration counter accurately reflects progress.

  • RepeatUntil continues until the condition is true OR maximum iterations are reached
  • Loop body can contain multiple steps executed sequentially each iteration
  • Use [Append] reducers to accumulate history across iterations
  • Phase names include loop prefix to ensure uniqueness
  • Built-in infinite loop protection via maxIterations
  • Loops can be nested for complex multi-level refinement
  • Each iteration is persisted, enabling recovery mid-loop

You have learned how to iterate until quality thresholds are met. Some workflows need human input:

  • Approvals - Pause workflows for human review and sign-off
  • Agent Selection - Choose the best agent for each task dynamically