Skip to content

Loops

Any step can loop back to an earlier step, forming a cyclic loop body. Attach loop_back_to and loop_config to the tail step of the loop.

After the tail step completes successfully, the executor evaluates loop_config.condition_key; while it resolves truthy (and the iteration budget isn’t exhausted) the loop body — steps [loop_back_to, tail] inclusive — is reset and re-run.

FieldTypeDefaultPurpose
condition_keystr""A context reference ($memory.ns.key, $history[-1], $outer_context) whose resolved value is cast to bool. Empty = “always loop” up to max_iterations.
max_iterationsint10Budget guard — max re-executions of the loop body (≥ 1).
negate_conditionboolFalseFalse = while-loop (continue while truthy). True = until-loop (continue while falsy).
from mmar_carl import ToolStepDescription, ToolStepConfig, LoopConfig
# Steps 1–2 form the loop body; step 2 drives iteration.
ToolStepDescription(
number=2,
title="Refine answer",
config=ToolStepConfig(tool_name="refiner", input_mapping={}),
loop_back_to=1,
loop_config=LoopConfig(
condition_key="$memory.loop.needs_retry", # truthy → loop again
max_iterations=5,
),
)

For an until-loop (run until the flag becomes truthy), set negate_condition=True.

ChainBuilder wraps the manual API with add_until_loop and add_while_loop: pass a list of body steps and a condition_key, and the builder renumbers the body and attaches the right LoopConfig (add_while_loop continues while truthy; add_until_loop continues until truthy).

The repo’s loop example (no API key) shows a research loop that keeps searching until enough facts are gathered:

body = [
ToolStepDescription(number=0, title="Search Facts",
config=ToolStepConfig(tool_name="search_facts", input_mapping={})),
ToolStepDescription(number=0, title="Evaluate Research",
config=ToolStepConfig(tool_name="evaluate_research", input_mapping={})),
ToolStepDescription(number=0, title="Check Done",
config=ToolStepConfig(tool_name="check_done", input_mapping={})),
]
chain = (
ChainBuilder()
.add_until_loop(body_steps=body, condition_key="$metadata.step_3", max_iterations=10)
.build()
)

After the run, the per-step iteration counts are in context.metadata["loop_iteration_history"]. The example also covers a retry-until-success loop, the manual loop_back_to / loop_config API, and an add_while_loop queue drainer.