← Back to Agentish Framework Guide

Chapter 7 Loops

Iterative workflows done right

Some tasks need iteration: analyze, evaluate, refine, repeat. Agentish supports loops through Router back-edges — when a Router sends execution back to an earlier node in the graph. This chapter explains how to build loops that work correctly and don’t get stuck.

How Loops Work

A loop in Agentish requires exactly three things:

  1. A Router Node that evaluates whether to continue or exit.
  2. A back-edge from the Router to an earlier LLM Node (the loop target).
  3. An exit edge from the Router to a downstream node (the exit path).
Canonical loop pattern — Entry → Orchestrator → Router, with back-edge to Orchestrator (retry) or exit to Finalizer (done)

The Router decides: “Is the work good enough?” If not → send back to Orchestrator for another pass. If yes → send to Finalizer to produce final output.

Critical: Loops without a Router are infinite loops. If LLM A connects to LLM B connects back to LLM A with no Router in between, there is no decision point to break the cycle. The validator will block this.
Reminder — one outgoing LLM connection: Even inside a loop, an LLM Node’s output connects to exactly one downstream node — either one other LLM node or one Router node. It cannot connect to multiple nodes simultaneously. The loop back-edge is created by the Router, not by the LLM directly wiring itself to multiple targets.

The Problem: What Happens on Re-Entry?

When execution loops back to an LLM Node, a question arises: what conversation history should the LLM see?

This is why loop mode exists. It tells the compiler how to manage conversation history for LLM Nodes that are loop targets.

Loop Mode

When an LLM Node has more than one incoming edge (one from upstream, one from a Router back-edge), the Loop mode field appears in its inspector. You must set it to one of two values:

ModeBehaviorBest For
Fresh Restarts the conversation from scratch on each re-entry. The LLM gets a clean system prompt + human prompt, as if it’s running for the first time. If a feedback state variable is set, its value is injected as the human message instead of the original human prompt. When you want each iteration to be independent. The LLM doesn’t see what it did in previous passes — only the feedback from the Router telling it why it was sent back.
Continue Extends the accumulated conversation history. The LLM sees everything from all previous iterations plus any new feedback. History grows with each pass. When the LLM needs to build on previous work. Good for incremental refinement where earlier context matters.
Required: If a loop target LLM Node does not have loop_mode set, the topology validator will block export with an error. This is enforced because the default behavior (replaying stale history unchanged) produces identical output on every iteration — a guaranteed infinite loop.

Feedback State Variable

The Feedback state variable field appears when loop mode is set. It selects a state variable whose value is injected as a new human message on each re-entry.

This is how you give the LLM new context on each loop iteration. Typically, this is a variable written by the Router or an upstream node explaining why execution was sent back.

Example: Feedback-Driven Loop
# State variables: analysis_result str # Written by Orchestrator routing_reason str # Written by Router (why it looped back) final_report str # Written by Finalizer # Orchestrator config: loop_mode: "fresh" loop_feedback_state_key: "routing_reason" # What happens on re-entry: # Pass 1: Orchestrator runs with original prompts → produces analysis # Router: "Analysis is too short" → writes routing_reason, loops back # Pass 2: Orchestrator gets clean context + "Analysis is too short" # as its human message → produces better analysis # Router: "Analysis is comprehensive" → exits to Finalizer
Fresh + no feedback: If fresh mode is set but no feedback variable is specified, the LLM falls back to its original human prompt on re-entry. This works but means the LLM gets no information about why it was sent back.
Continue + no feedback: The validator will emit a warning if continue mode is set with no feedback variable. The LLM will re-run with its accumulated history but no new context injected — this may work for some use cases but risks stagnation.

Exit Conditions

Every loop must have a clear exit path. This is enforced at two levels:

1. Topology Validation (Compile-Time)

2. Router Prompt (Runtime)

The topology check only verifies that an exit path exists. Whether the Router actually uses it depends on its system prompt. Write clear exit criteria:

Good Router Prompt for Loops
"You are a quality evaluator. Based on the analysis_result: - Route to 'Orchestrator' if: - The analysis is under 200 words - Key aspects are missing or vague - The result explicitly says more work is needed - Route to 'Finalizer' if: - The analysis is comprehensive (200+ words) - All major aspects are covered - The result is ready for final formatting When in doubt after 3+ iterations, route to Finalizer to avoid endless loops."

Common Mistakes

MistakeWhat HappensFix
Loop without a Router Infinite loop. Validator blocks export. Always use a Router to control loop iteration.
Router with no exit edge Infinite loop. Validator blocks export. Ensure at least one Router output goes to a node outside the cycle.
No loop_mode on target node Validator blocks export. Set loop_mode to “fresh” or “continue” in the LLM Node inspector.
Vague Router exit criteria Router loops forever at runtime (no validator catch). Write specific, measurable exit conditions in the Router prompt. Include a fallback (“after N iterations, exit”).
Fresh mode + no feedback variable LLM gets the same prompt every time, may produce same output. Set a feedback state variable so each iteration gets new context.

ASL Example: Complete Loop

ASL — Orchestrator–Router Loop
{ "graph": { "entrypoint": "1", "state": { "schema": { "analysis_result": "str", "final_report": "str", "routing_reason": "str" } }, "nodes": [ { "id": "1", "type": "EntryPoint", "label": "EntryPoint", "config": { "title": "Entry Node", "initial_state": { "analysis_result": "str", "final_report": "str", "routing_reason": "str" } } }, { "id": "2", "type": "LLMNode", "label": "Orchestrator", "config": { "title": "Orchestrator", "system_prompt": "You are an orchestrator...", "human_prompt": "Analyze the problem...", "loop_mode": "fresh", "loop_feedback_state_key": "routing_reason", "output_state_keys": ["analysis_result"], "structured_output_schema": [ { "name": "analysis_result", "type": "str", "description": "Analysis findings" } ] } }, { "id": "3", "type": "RouterBlock", "label": "Router", "config": { "title": "Router", "input_state_keys": ["analysis_result"], "system_prompt": "Evaluate the analysis...", "router_values": [ { "node": "Orchestrator", "description": "Analysis needs refinement." }, { "node": "Finalizer", "description": "Analysis is complete." } ] } }, { "id": "4", "type": "LLMNode", "label": "Finalizer", "config": { "title": "Finalizer", "system_prompt": "Produce the final report...", "output_state_keys": ["final_report"] } } ], "edges": [ { "from": "1", "to": "2", "type": "NormalEdge" }, { "from": "2", "to": "3", "type": "NormalEdge" }, { "from": "3", "to": "2", "type": "ConditionalEdge", "condition": "Orchestrator" }, { "from": "3", "to": "4", "type": "ConditionalEdge", "condition": "Finalizer" } ] } }

Note the highlighted loop_mode and loop_feedback_state_key on the Orchestrator. The Router can loop back to Orchestrator or exit to Finalizer.

Chapter Summary

Key Takeaways:
← Chapter 6: Connecting Nodes Chapter 8: State →