Orchestration is the part of an agent system that decides what happens next: which tool to call, whether to retry, when to stop and ask a human. It sounds abstract until the agent gets stuck in a loop calling the same failing tool five times, or stops one step short of finishing a task, and you realize there was no explicit control flow — just an LLM improvising the next step every time with no bounds on how long it could keep going.
The loop underneath every agent framework
Strip away the framework branding and every agent orchestration pattern is a loop: call the model, inspect whether it wants to call a tool, execute the tool if so, feed the result back, repeat until the model produces a final answer or you hit a stop condition. That loop, written directly, is maybe 30 lines of code. Frameworks add retry policies, parallel tool execution, and state persistence around that loop — genuinely useful once you need them, but the loop itself doesn't require a dependency to understand or debug.
def run_agent(user_message: str, max_steps: int = 8) -> str:
messages = [{"role": "user", "content": user_message}]
for step in range(max_steps):
response = call_model(messages, tools=TOOL_SCHEMAS)
if not response.tool_calls:
return response.content # model is done, no more tools needed
for call in response.tool_calls:
result = execute_tool(call.name, call.arguments) # validated, logged
messages.append(tool_result_message(call.id, result))
return "Reached max steps without a final answer — escalating to a human."
Bounding the loop is not optional
Every production agent needs a hard step limit and a hard time limit, enforced in code, not requested in the prompt. An LLM that decides to double-check the same fact three different ways will happily burn your entire latency and cost budget doing it if nothing stops it. When the limit is hit, the fallback should be a clear "I couldn't complete this, here's what I tried" handed to a human — not a silent failure and not an infinite retry.
Sequential vs parallel steps
Not every tool call needs to wait for the previous one. If a task needs the customer's credit status and their open order count, and neither depends on the other, fetch both concurrently rather than serially — it's a straightforward asyncio.gather, not a reason to adopt a graph-based orchestration framework. Reserve genuine sequential dependency (fetch the order, then validate against its actual line items) for cases where step 2 truly needs step 1's output.
A tool call that failed because of a transient network error should retry with backoff. A tool call that failed because the model passed an invalid parameter should not retry blindly — it should go back to the model with the error message so it can correct the argument, capped at one or two correction attempts before giving up. Treating both failure types the same way either wastes retries or gives up too early.
State that survives a restart
For anything that runs longer than a single request-response cycle — a multi-step approval workflow, a batch job processing hundreds of records — persist the loop's state (which step it's on, what's been done, what's pending) somewhere durable, not just in memory. If the process restarts mid-task, you want to resume from the last completed step, not silently lose a half-finished workflow or, worse, restart it and duplicate the tool calls that already ran.
When a framework earns its place
Reach for LangGraph, a durable-execution engine, or similar once you need: state that survives process restarts across long-running workflows, complex branching that a plain loop makes hard to read, or a team standard so multiple engineers write orchestration the same way. None of that is required for the common case of "user asks something, agent calls a couple of tools, agent answers" — that case is the 30-line loop above with a step cap and a timeout, and adding a framework for it just adds a debugging layer between you and what actually happened.
| Situation | Right tool |
|---|---|
| Single request, a few tool calls, done | Plain loop with step/time limits |
| Independent lookups | Concurrent calls (asyncio.gather), not sequential |
| Multi-day workflow, needs to survive restarts | Durable execution engine or persisted state machine |
| Complex conditional branching, multiple engineers | Graph-based framework, for the shared vocabulary |
Wrapping up
Orchestration is control flow with an LLM in the loop — treat it with the same discipline you'd apply to any retry logic: hard bounds, explicit failure paths, and state that survives a crash when the task is long-running. Start with the plain loop, add a framework only when you can name the specific capability (durable state, complex branching, team standardization) it gives you that the loop doesn't.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.