Skip to content

Validation Loop Pattern

Purpose

Ensure quality by verifying results and retrying if they don’t meet criteria. Prevents low-quality outputs from proceeding.

Structure

[action] → [check] → success → [next]
failure → [fix] → [increment-iteration] → [action]

Implementation

Action Node

{
"type": "agent-directive",
"id": "do-work",
"directive": "Complete the task. Iteration: {{current_iteration}}",
"completionCondition": "Task completed with quality standards met",
"inputSchema": {
"type": "object",
"properties": {
"result": { "type": "string" },
"quality_check_passed": { "type": "string", "enum": ["yes", "no"] }
},
"required": ["result", "quality_check_passed"]
},
"connections": { "success": "check-quality" }
}

Check Node

{
"type": "condition",
"id": "check-quality",
"condition": {
"operator": "eq",
"left": { "contextPath": "quality_check_passed" },
"right": "yes"
},
"connections": {
"true": "next-step",
"false": "fix-issues"
}
}

Fix Node

{
"type": "agent-directive",
"id": "fix-issues",
"directive": "Fix issues found in iteration {{current_iteration}}. Previous result: {{result}}",
"connections": { "success": "increment-iteration" }
}

Iteration Counter

Using expression node:

{
"type": "expression",
"id": "increment-iteration",
"expressions": ["current_iteration = current_iteration + 1"],
"connections": { "default": "do-work" }
}

Always include iteration counters to prevent infinite loops. Consider adding a max iteration check.

With Max Iterations

Add check before retry:

{
"type": "condition",
"id": "check-max-iterations",
"condition": {
"operator": "lt",
"left": { "contextPath": "current_iteration" },
"right": 5
},
"connections": {
"true": "do-work",
"false": "escalate-to-user"
}
}

Bounded Re-Validation Loop

A re-validation or re-review loop revisits the SAME validate node on each round. Without a cue in the loop-entry directive, the agent can misread seeing the same node twice as the flow being stuck and report a loop to the user. Two requirements prevent this.

Counter is an Expression Node

The round counter MUST be incremented by an expression node (flow-automatic), declared in variableRegistry with a numeric default. An agent-incremented counter is the anti-pattern — the agent is not asked to do arithmetic, and a missing or non-numeric value breaks the bound check.

{
"variableRegistry": {
"validation_round": {
"type": "number",
"description": "Re-validation pass counter",
"default": 0
},
"max_validation_rounds": {
"type": "number",
"description": "Re-validation bound",
"default": 5
}
}
}
{
"type": "expression",
"id": "increment-validation-round",
"expressions": ["validation_round = validation_round + 1"],
"connections": { "default": "re-validate" }
}

Loop-Entry Cue

The loop-entry directive renders the round counter and states that the repetition is expected:

{
"type": "agent-directive",
"id": "re-validate",
"directive": "Re-validation pass {{validation_round}} of {{max_validation_rounds}} — a normal quality loop, expected to converge. This is NOT a bug and NOT a stuck flow; do not report a loop to the user.\n\nRe-check the work against the criteria and report whether all issues are resolved.",
"completionCondition": "Work re-checked against criteria",
"inputSchema": {
"type": "object",
"properties": {
"all_resolved": { "type": "string", "enum": ["yes", "no"] }
},
"required": ["all_resolved"]
},
"connections": { "success": "check-resolved" }
}

The cue text is exact. Re-validation passing through the same node is expected, not a bug — the directive must say so, or the agent may stop and report a false loop.

Numeric Quality Check

For a property a machine can decide, route on the machine’s own answer — a count of reproducible findings, an exit status, a coverage gap:

{
"inputSchema": {
"properties": {
"issues_count": { "type": "number", "minimum": 0 }
},
"required": ["issues_count"]
}
}
{
"condition": {
"operator": "eq",
"left": { "contextPath": "issues_count" },
"right": 0
}
}

Do not route on a quality score. A number a reviewer assigns to coherence, depth or architecture does not distinguish two states of the result — seven against eight says nothing a reader could verify — and the threshold is met by adjusting the number. Where the property is qualitative, the gate is the reviewer’s reasoned verdict and the count of findings that verdict produced. See Fixed Semantic Quality Score.

Real Example

From development-flow.json:

{
"id": "verify-step-implementation",
"directive": "Verify step {{current_step_name}} implementation:\n- Expected: {{expected_outcome}}\n- Check actual matches expected",
"inputSchema": {
"properties": {
"step_verified": { "type": "string", "enum": ["yes", "no"] },
"verification_evidence": { "type": "string" }
},
"required": ["step_verified", "verification_evidence"]
}
}

Agent Behavior at Validation Gates

When a workflow includes validation gates (nodes that ask user for approval), the agent must follow strict rules:

Validation Gate Directive Pattern

A gate directive says what its answer records and where each answer leads, so the agent can see that answering honestly is the only way the run proceeds correctly:

`approval` records the user's own answer. Anything other than an unqualified yes is a no, and the
feedback belongs in `user_feedback` — the flow routes a no to the responsibility that owns the
repair, so a correction made here instead is a change nobody reviewed.

Why This Matters

A gate exists to move a decision to whoever owns it. An agent that reads the answer as “minor corrections I can apply myself” delivers work the flow never routed through its repair and review path, and an approval = "yes" recorded over feedback loses the feedback entirely. Naming the consequence — where a no goes, what a yes closes — is what makes the honest answer the useful one.

Example Validation Gate

{
"type": "agent-directive",
"id": "approve-plan",
"directive": "Present the plan and ask whether it is approved.\n\nPlan: {{plan_summary}}\n\n`approval` records the user's own answer: anything other than an unqualified yes is a no, with the exact words in `user_feedback`. A no routes to the responsibility that owns the revision, so a correction made here instead is a change nobody reviewed.",
"completionCondition": "User confirmed or rejected plan",
"inputSchema": {
"type": "object",
"properties": {
"plan_approved": { "type": "string", "enum": ["yes", "no"] },
"user_feedback": { "type": "string" }
},
"required": ["plan_approved"]
},
"connections": { "success": "route-plan-approval" }
}