I used to wrestle with keeping my Agentic AI workflows clean, testable, and actually scalable. Then I started building around MartinLoop, and honestly, it changed how I think about the whole process. Here's what I learned and why I'm not going back
MartinLoop is a loop-oriented orchestration layer for agentic AI workflows. That's the one-sentence version, but it's worth unpacking because "orchestration layer" gets thrown around a lot and usually means something different depending on who's saying it. Here's what MartinLoop actually does: It manages a central execution loop that drives agents through a defined sequence of steps It passes typed context between agents, enforcing schema at every boundary It handles branching logic, termination conditions, and retry behavior as first-class primitives It records a structured trace of every agent invocation, tool call, and iteration It supports inserting human approval steps into a flow without restructuring the surrounding pipeline What it doesn't do: it doesn't provide LLM API access, it doesn't manage vector storage, and it doesn't write your agent logic for you. It's the runtime that coordinates agents you've already defined, nothing more. The mental model that clicked for me was thinking of it less like a workflow engine (think Airflow, Prefect, or Temporal) and more like a runtime for agent conversations. A workflow engine cares about tasks and dependencies. MartinLoop cares about agents, their roles, the context they share, and when the loop should stop. The Central Concept: The Agent Loop Everything in MartinLoop is organized around the loop. Not in the vague sense of "agents do things repeatedly," but as a concrete execution model with a specific lifecycle. Each tick of the loop does the following: Selects the next agent to run based on the defined flow or a router decision Projects the current context down to only the fields that agent needs Invokes the agent with its tools available Collects the agent's output and merges it back into the shared context Evaluates the termination condition against the updated context Either continues to the next agent or halts That fifth step is where a lot of the value hides. In my previous setup, I was managing loop termination by hand, which meant either baking stop logic into agent prompts (bad) or writing fragile wrapper code around every pipeline (also bad). MartinLoop's terminationCondition is a simple predicate on the context. When it returns true, the loop stops. On my knowledge-base pipeline, moving to a proper termination condition cut average iterations from 8 down to 4 per query, just by stopping the loop when the context actually signaled completion rather than running to the iteration ceiling every time. Here's what the full loop setup looks like in Java 25: import com.martinloop.AgentLoop; import com.martinloop.Agent; import com.martinloop.AgentTransition; import com.martinloop.Context; import java.util.List; import java.util.Optional; record ResearchContext( String query, Optional<List<String>> findings, Optional<String> summary, boolean summaryComplete ) implements Context { ResearchContext { if (query == null || query.isBlank()) { throw new IllegalArgumentException("Query must not be blank"); } } static ResearchContext initial(String query) { return new ResearchContext(query, Optional.empty(), Optional.empty(), false); } ResearchContext withFindings(List<String> newFindings) { return new ResearchContext(query, Optional.of(newFindings), summary, summaryComplete); } ResearchContext withSummary(String newSummary) { return new ResearchContext(query, findings, Optional.of(newSummary), true); } } public class ResearchPipeline { public static void main(String[] args) { var researcher = Agent.builder() .name("researcher") .model("gpt-4o") .systemPrompt(""" You are a research assistant. Given a query, find relevant information using available tools and return structured findings. """) .tools(new WebSearchTool(), new FetchUrlTool()) .contextFields("query") // only sees what it needs .build(); var synthesizer = Agent.builder() .name("synthesizer") .model("gpt-4o") .systemPrompt(""" You are a synthesis expert. Given research findings, produce a concise summary and set summaryComplete to true. """) .contextFields("query", "findings") // doesn't see raw URLs .build(); var loop = AgentLoop.<ResearchContext>builder() .agents(List.of(researcher, synthesizer)) .flow(List.of( AgentTransition.of("researcher", "synthesizer") )) .maxIterations(10) .terminationCondition(ctx -> ctx.summaryComplete()) .build(); var result = loop.run(ResearchContext.initial( "What are the latest findings on transformer efficiency?" )); System.out.println(result.finalOutput()); } } Notice the contextFields on each agent. That's MartinLoop's per-agent context projection at work. The synthesizer never sees the raw URL list the researcher fetched. It sees the findings and the original query, and nothing else. This matters for cost reasons I'll get to, but it also matters for correctness: agents make worse decisions when they're handed context they don't know what to do with. How MartinLoop Handles Context Context management is probably the feature I use most and think about least now, which is exactly how it should be. MartinLoop's Context interface is designed to be implemented by Java records. The framework treats context as immutable state that flows through the loop, with each agent receiving a validated snapshot and returning an updated one. The type system does the enforcement. In my old setup, I was passing Map<String, Object> between agents. The schema drifted constantly. One class would write sourceUrls , another would read sources , and I'd spend twenty minutes tracing a NullPointerException back to a rename I'd made three weeks earlier. Worse, because nothing was typed, agents would receive the entire map and pass it wholesale to the model. Nobody was filtering because nobody had the information needed to filter. With typed records, that problem disappears at compile time. And with per-agent contextFields , MartinLoop handles the filtering automatically. The framework serializes only the declared fields into each agent's prompt. My synthesizer was consuming 3,000 input tokens per call because it was getting the full raw research dump. After I defined its context projection, that dropped to around 800 tokens. At gpt-4o pricing ($2.50 per million input tokens), that's a 73% reduction in input cost for that agent alone. How MartinLoop Handles Routing MartinLoop supports two routing modes, and you pick the one that fits your pipeline. Static flow is what most of my pipelines use. You define transitions between agents explicitly, and the loop follows them. Deterministic, testable, easy to reason about. If researcher always feeds synthesizer, that's one line of configuration. Router agent mode lets the LLM decide which agent to invoke next. You define a router agent that receives the current context and returns the name of the next agent to run. This is useful when the pipeline is genuinely conditional and you'd rather not encode every branch in configuration. I've used it for pipelines where the right next step depends on what the previous agent found, and it works, but the documentation is still thin and I've hit context-truncation issues when the agent list gets long. It's marked experimental for a reason. I'd use it carefully. How MartinLoop Handles Observability The built-in trace system is what changed how I debug and how I think about token costs. Every loop iteration produces a structured trace entry. Every agent invocation records i...