Chapter 1 · Lesson 3

Workflows vs. Agents (and where a framework fits)

Decide when to hard-code the control flow (a workflow) versus letting the LLM drive (an agent) — and see why LangGraph models both as graphs.

More autonomy isn't always better

It's tempting to make everything an agent. But for a task like "summarize this support doc," a fixed pipeline is cheaper, faster, and never goes rogue — there's no loop where the model can wander off, call the wrong tool, or hallucinate a step. The senior skill isn't building the most autonomous system you can. It's knowing when to hand the model the wheel — and when to keep it firmly in the code.

Northwind's Aria has to handle both kinds of work: rigid, regulated flows (issue a refund within policy) and open-ended ones (research why a customer churned). The same framework should serve both. That's the bet LangGraph makes.

Two ways to control the flow

Every multi-step LLM system answers one question: who decides what happens next — your code, or the model?

  • Workflowyour code controls the path. The steps are predefined and wired in advance: do A, then B, then C. The LLM is a worker called at fixed points; it never chooses the route.
  • Agentthe LLM controls the path. You give it a goal and a set of tools, and it decides which tool to call, in what order, and when it's done. The loop runs until the model says "finished."

This is a reliability ↔ flexibility trade-off:

Workflow (code drives)Agent (LLM drives)
PathFixed, known in advanceDecided at runtime
ReliabilityHigh — same path every timeLower — can take odd routes
FlexibilityLow — only the cases you codedHigh — adapts to novel inputs
Cost / latencyPredictable, usually lowerVariable — extra LLM turns
Best whenSteps are knownSteps are not known
The agent loop START flows into an LLM call, then a tool-call decision diamond; if yes it runs a tool node and loops back to the LLM call, otherwise it replies and ends. A glowing token travels the cycle. yes — call tool feed result back no — reply START llm_call reason & decide tool call? tool_node execute action END final reply
An agent is a loop: the LLM decides whether to call a tool, the result feeds back in, and it keeps going until it's done. A workflow is the same graph with the edges fixed by you.
🔍 Under the hood — why LangGraph spans both

LangGraph represents everything as a graph of nodes and edges over a shared state. A workflow is a graph whose edges you draw (static routing). An agent is a graph whose routing edge is decided by the model each turn (the tool-calling loop). Same primitive — StateGraph — so you can start with hard-wired edges and later swap one edge for a model decision without a rewrite.

Worked example: "process a refund," two ways

Same task, both control styles. First, the workflow — you know the three steps (look up the order, check the policy, issue the refund), so you wire them in code. The model is used only inside check_policy to make a judgement; it never picks the route.

from langgraph.graph import StateGraph, START, END
from langchain.chat_models import init_chat_model
from typing import TypedDict

llm = init_chat_model("claude-sonnet-4-5-20250929", model_provider="anthropic")

class RefundState(TypedDict):
    order_id: str
    order: dict
    approved: bool
    result: str

def lookup_order(state: RefundState) -> dict:
    return {"order": db.get_order(state["order_id"])}        # plain code

def check_policy(state: RefundState) -> dict:
    verdict = llm.invoke(
        f"Refundable under our 30-day policy? Order: {state['order']}. "
        f"Answer only YES or NO."
    ).content
    return {"approved": "YES" in verdict.upper()}            # LLM as a worker

def issue_refund(state: RefundState) -> dict:
    if state["approved"]:
        return {"result": payments.refund(state["order"])}   # plain code
    return {"result": "Refund denied: outside policy window."}

# YOU draw the edges — the path is fixed
builder = StateGraph(RefundState)
builder.add_node("lookup", lookup_order)
builder.add_node("policy", check_policy)
builder.add_node("refund", issue_refund)
builder.add_edge(START, "lookup")
builder.add_edge("lookup", "policy")
builder.add_edge("policy", "refund")
builder.add_edge("refund", END)
workflow = builder.compile()

Now the agent. You hand the model the same capabilities as tools and let it decide the order. Notice you no longer draw edges between steps — the loop does that.

from langchain.tools import tool
from langchain.agents import create_agent

@tool
def lookup_order(order_id: str) -> dict:
    """Fetch an order record by its ID."""
    return db.get_order(order_id)

@tool
def issue_refund(order_id: str) -> str:
    """Refund an order. Only call after confirming it meets the 30-day policy."""
    return payments.refund(db.get_order(order_id))

agent = create_agent(
    model="anthropic:claude-sonnet-4-5-20250929",
    tools=[lookup_order, issue_refund],
    system_prompt=(
        "You are Aria, Northwind's support agent. "
        "Refund an order only if it is within the 30-day policy window."
    ),
)

agent.invoke({"messages": [{"role": "user", "content": "Refund order A-1003."}]})

The agent might look up the order, reason about the policy, then refund — or it might ask a clarifying question, or refuse. That adaptability is the point. So is the cost: it's at least two LLM turns, and the route isn't guaranteed.

⚠️ Common pitfall — reaching for an agent too early

If your refund flow is always "look up → check policy → refund," an agent buys you nothing but extra latency, token cost, and a chance to misroute. Hard-coded steps that you understand should stay hard-coded. Add autonomy only where the path genuinely varies.

Your turn

🧪 Try it — workflow or agent?

For each task, decide workflow or agent, and justify in one sentence:

  1. Summarize an incoming support ticket into three bullet points.
  2. Investigate why a customer's last three orders were delayed, pulling from orders, shipping, and notes as needed.
  3. Translate every reply Aria sends into the customer's language before it goes out.

How you did

1 — Workflow. One known step (summarize). No routing decision exists, so don't create one. A single LLM call inside a fixed pipeline.

2 — Agent. You can't predict which sources are needed or in what order — that depends on what each lookup reveals. This is exactly the "steps aren't known" case.

3 — Workflow. "Translate, then send" is a fixed final step on every reply. Wire it in code.

🧭 Decision point — your rule of thumb

Prefer the least autonomy that solves the problem. Start as a workflow. Promote a step to agent-driven only when you genuinely can't enumerate the path in advance. Because LangGraph models both as graphs, that promotion is a small edit — not a rebuild.

Takeaway

Lesson Summary
Use a workflow when the steps are known; reach for an agent when they aren't — and let LangGraph carry you from one to the other.