01. The Autonomy Spectrum
There are two main ways to build a system. Workflows have predetermined code paths. They are designed to operate in a certain order. Agents are different. They are dynamic. Agents define their own processes and tool usage. Workflows give you a fixed sequence that runs step by step. Agents can adapt and choose their own path as they go. Many systems actually combine both ideas. You get the predictability of a fixed plan with the flexibility to change based on new information. That blend is common in real applications. For example, an agent might use a workflow for routine checks. That keeps things reliable. Then it switches to dynamic decision making for open ended problems. This way you do not lose control. But you also get the ability to handle unexpected tasks. So think of it as a spectrum. On one end you have strict workflows. On the other end you have free agents. Most real systems sit somewhere in between. They mix the two approaches to get the best of both worlds.
An agent-style loop that iteratively improves a joke based on evaluation feedback.
@task
def llm_call_evaluator(joke: str):
"""LLM evaluates the joke"""
feedback = evaluator.invoke(f"Grade the joke {joke}")
return feedback
@entrypoint()
def optimizer_workflow(topic: str):
feedback = None
while True:
joke = llm_call_generator(topic, feedback).result()
feedback = llm_call_evaluator(joke).result()
if feedback.grade == "funny":
break
return joke
Imagine a restaurant where a head chef decides whether to cook everything solo or call in a specialist pastry chef for desserts. This chapter is about building computer systems that coordinate multiple specialists—called agents—to handle complex jobs, but you get to choose how much control the head chef keeps. The system can follow a fixed recipe step by step (a workflow), or let each specialist decide their own cooking steps (an agent), and many real kitchens mix both.
The source lays out exact patterns for this. In Subagents, the head chef always assigns a task to a line cook, waits for the result, then passes it along—each cook starts fresh every order. In Handoffs, a cook can hand a ticket directly to another station, and that station takes over completely, staying active for repeat orders. Skills let the head chef pull up a recipe card when needed without handing off. Router uses a host who decides which chef gets the order, then the chosen chef works alone. These patterns differ in how many communication calls they need and whether they keep memory (state) between turns. For example, on a repeated request like "buy coffee again," Handoffs and Skills save 40–50% of calls because state persists, while Subagents repeat the full four-call cycle each time.
The non‑obvious edge is statefulness: Handoffs and Skills reuse loaded context, making them cheaper for repeated tasks, but Subagents intentionally wipe state each time to isolate each agent’s knowledge. Without these patterns, you’d either cram every tool into one agent—causing poor tool choices—or build a rigid one‑recipe system that cannot adapt to new orders, exactly the failure a beginner would feel when their system stops working for an unexpected request.
In the Subagents pattern, the mechanism proceeds in a fixed order: the main agent, created via create_agent with a model and tools, receives the user’s request and decides whether to invoke a subagent as a tool. Each subagent runs in its own isolated context, executes its own tool calls, and returns results to the main agent. The main agent then integrates those results into its next reasoning step. If a subagent invocation fails (e.g., the subagent tool returns an error), the main agent receives that error as a tool output and can either retry, escalate, or continue with a fallback. This ordered chain ensures that all routing and final responses pass through the main agent.
The invariant preserved by this design is that the main agent is the single point of coordination and context maintenance. Subagents are explicitly stateless by design; each invocation follows the same flow, and the main agent holds all conversation history across turns. This guarantees that no subagent can alter the conversation state or bypass the main agent’s oversight. The source names this as the central property: “the main agent maintains conversation context”.
The key trade‑off is an extra LLM call per subagent invocation compared to alternatives like Handoffs or Skills (4 calls versus 3), as seen in the repeat‑request table. The obvious alternative—allowing subagents to respond directly to the user—was rejected because it would decentralize control and risk fragmenting context. The cost this rejection avoids is the complexity of synchronizing state across multiple agents and losing the ability to enforce sequential constraints or centralized error handling. As the source states, “this overhead provides centralized control.”
A concrete failure mode is a subagent tool call that times out or returns nonsensical output. The operator would observe, via LangSmith traces, that the subagent invocation produced an error or unexpected response, while the main agent’s subsequent reasoning may be corrupted. The trace would show the subagent’s raw output and the main agent’s decision to proceed or fail—providing a clear signal of where the breakdown occurred.
Q (warm-up): In the autonomy spectrum, what distinguishes a fixed workflow from an autonomous agent according to the source materials?
A The fixed workflow is exemplified by the router_workflow function, which uses llm_call_router and a set of if-elif branches to pre‑determine the next step (story, joke, or poem). In contrast, an autonomous agent pattern like Handoffs allows agents to transfer control to each other via tool calls, adapting their path dynamically. The key difference is that workflows have predetermined code paths, while agents define their own processes and tool usage as they execute.
Follow-up Why would you still choose a fixed workflow over an autonomous agent for a simple task?
A Fixed workflows provide predictable, stateless execution with no risk of context drift, and they incur the same number of calls as autonomous patterns for single tasks (e.g., 3 calls for both Router and Handoffs in the single-task comparison).
Weak answer misses The concrete identifier router_workflow and the use of llm_call_router for branching; a shallow answer might just say “workflows are predictable” without naming the exact function.
Q (medium): In the Subagents pattern, why is there an extra LLM call compared to the Handoffs pattern for a single task?
A The source explicitly states: “Subagents adds one extra call because results flow back through the main agent—this overhead provides centralized control.” The Subagents pattern uses a main agent that coordinates subagents as tools, so each subagent’s output must first return to the main agent before a final response is generated, requiring 4 calls versus 3 for Handoffs.
Follow-up What specific benefit does that extra call bring?
A It provides strong context isolation: subagents are “stateless by design” and start fresh each time, ensuring no cross‑contamination between subagent contexts.
Weak answer misses The exact phrase “results flow back through the main agent” and the call counts (4 vs. 3) are the real details that a vague answer would omit.
Q (hard — design question): For multi‑domain tasks where each request is independent and one‑off, why use a stateless Router pattern instead of a stateful Handoffs pattern?
A The Router pattern is stateless—each request requires an LLM routing call from scratch, as described: “Routers are stateless—each request requires an LLM routing call.” For one‑off tasks, the statefulness of Handoffs provides no advantage, and the Router’s stateless design gives strong context isolation with no risk of leftover state affecting the next unrelated domain. Additionally, the source shows that for a single task, both patterns use exactly 3 LLM calls, so there is no performance penalty.
Follow-up Under what condition would the Handoffs pattern become significantly more efficient than Router?
A On repeat requests (e.g., “Buy coffee” twice), Handoffs saves 40–50% of calls because the coffee agent remains active, avoiding a handoff; Router statelessly repeats the full 3‑call flow each time.
Weak answer misses The specific cost comparison for repeat requests (2 calls vs. 3) and the characterization of Routers as stateless—not just “simpler.”
Q (hard): The orchestrator‑worker pattern is described as providing flexibility when subtasks cannot be predefined. How does this pattern illustrate the blend of fixed workflow and autonomous agent behavior?
A The orchestrator has a fixed workflow: it breaks down tasks into subtasks, delegates to workers, and synthesizes outputs—explicitly listed in the source as “Breaks down tasks into subtasks / Delegates subtasks to workers / Synthesizes worker outputs into a final result.” However, the workers themselves are autonomous (they generate content via LLM calls), and the orchestrator is used “when subtasks cannot be predefined the way they can with parallelization.” This blend gives the predictability of a predetermined orchestration plan with the flexibility of dynamic worker execution.
Follow-up What concrete data structure in the source formalizes the orchestration plan?
A The Sections model (with a list of Section objects containing name and description) is used to structure the subtasks before delegating to workers.
Weak answer misses The explicit three‑step description of the orchestrator workflow and the Sections/Section models—answers that only mention “break down tasks” without citing the schema miss the grounded mechanism.
Failure: Checkpoint Storage Growth
- Trigger – A long-running thread (e.g., multi-turn conversation) accumulates large state over many super-steps while using the default checkpointing that writes the full value of every state channel at each step.
- Guard –
DeltaChannel(referenced in the source as[DeltaChannel](https://reference.langchain.com/python/langgraph/channels/delta/DeltaChannel)and described as storing “only incremental deltas instead of the full accumulated value”). It is the recommended mitigation, though it is noted as beta and requireslanggraph>=1.2. - Posture – fail‑soft. The system continues to run, but storage growth degrades performance until limits are hit. The source states this “can produce significant storage growth over time” but does not abort execution.
- Operator signal – Increasing storage usage metrics on the checkpoint store; potential slowdown in graph execution due to larger writes.
- Recovery – Manual intervention: reconfigure the graph to use
DeltaChannelfor append‑heavy channels. No automatic retry or fallback is present in the source.
Failure: LLM Call Routing Failure
- Trigger – The
llm_call_routerfunction (from therouter_workflowexample inlanggraph-workflows-agents.md) returns a string that does not match any of the expected branch conditions ("story","joke","poem"). Theif-elif-elseblock lacks a default else clause, so the variablellm_callis never assigned. - Guard – None. The source shows no exception handler, validation, or fallback around the
llm_call_routercall or the subsequentifblock. - Posture – fail‑hard. The Python runtime raises an
UnboundLocalErrorwhen the code later tries to callllm_call(input_).result(). - Operator signal – A Python traceback error ending with
UnboundLocalError: local variable 'llm_call' referenced before assignment. - Recovery – Manual code change to add a default branch (e.g.,
else: llm_call = llm_call_1) or to validate the router output. No automatic retry or fallback value is provided.
Failure: Process Crash During Async Durability
- Trigger – Graph execution with
durability="async"(fromlanggraph-durable-execution.md) and the process crashes while the asynchronous checkpoint write is in flight, before the next step begins. - Guard – None. The source explicitly acknowledges the risk: “there's a small risk that LangGraph does not write checkpoints if the process crashes during execution.” The
durability="async"parameter is a persistence mode, not a guard that catches or handles the crash. - Posture – fail‑hard. The intermediate state is lost, and the graph cannot resume from that point. The source states “you cannot recover from system failures … that occur mid‑execution” for the
exitmode; forasyncit similarly implies no recovery from a crash. - Operator signal – Silent absence of the expected checkpoint; when restarting the graph after the crash, the state reverts to the last fully written checkpoint (if any), or the thread is missing.
- Recovery – Manual restart from the last durable checkpoint. No automatic retry or backoff exists in the source.
Failure: DeltaChannel API Breaking Change
- Trigger – Using
DeltaChannelinlanggraph>=1.2under the beta designation, then upgrading to a future release where the API (method signatures, constructor arguments, or behavior) changes incompatibly. - Guard – None. The source only provides a warning: “
DeltaChannel… is currently in beta. The API may change in future releases.” No validation, deprecation path, or fallback is shown. - Posture – fail‑hard. Running the same code after the upgrade results in runtime errors (e.g.,
TypeErrororAttributeError) because the old API is no longer supported. - Operator signal – Python error traceback referencing
DeltaChannel(e.g.,TypeError: DeltaChannel.__init__() got an unexpected keyword argument). - Recovery – Manual code revision to match the new API. The source does not describe any automatic migration.
Failure: Handoff Failure in Multi‑Agent Pattern
- Trigger – In the handoffs pattern (
langchain-multi-agent.md), an agent attempts to transfer control to another agent via a tool call (e.g., calling thebuy_coffeetool on behalf of the coffee agent). The target agent’s state is unavailable or crashes during the handoff. - Guard – None. The source describes the handoffs pattern as “Agents transfer control to each other via tool calls” but does not include any exception handler, retry logic, or fallback for a failed handoff.
- Posture – fail‑hard. The tool call fails or the handoff never completes, leaving the user without a response.
- Operator signal – An error from the tool call (e.g.,
ToolExecutionErroror a silent timeout) or a missing subsequent response in the trace. - Recovery – Manual inspection and restart of the thread or agent. No automatic retry or fallback value is provided in the source.