Chapter 8 · 6 lessons
LangGraph, LangChain & Agent Workflows
When to build a workflow vs an agent, LangGraph state/reducers/checkpoints, interrupts and human approval, LangChain create_agent middleware, and Deep Agents.
LangChain / LangGraph
Show all 6 lessonsHide list
Lesson 1 of 6
Workflow vs Agent
A workflow is code-defined control flow with LLM calls at certain steps. An agent lets the model choose the next step. Start with the simplest thing that works: a single call, then a chain, then a routed workflow, and only then an autonomous agent.
| Pattern | Shape | Use when |
|---|---|---|
| Prompt chain | A → B → C | Fixed steps, each easier with the previous output |
| Router | classify → branch | Distinct input types need different handling |
| Parallel / map-reduce | fan out → merge | Independent subtasks or voting |
| Orchestrator-workers | plan → dispatch → combine | Subtasks unknown until runtime |
| Evaluator-optimizer | generate ↔ critique | Clear quality criteria, iterative refinement |
| Autonomous agent | loop with tools | Open-ended tasks where steps cannot be predicted |
Signal
Default to a workflow and justify autonomy. Interviewers reward candidates who reduce nondeterminism where they can.
Lesson 2 of 6
LangGraph Fundamentals
LangGraph models an agent as a graph over shared state. You define a state schema, nodes (functions that return partial state updates), and edges (fixed or conditional). Reducers decide how updates merge — for example, appending messages instead of overwriting them.
class State(TypedDict):
messages: Annotated[list, add_messages] # reducer appends
plan: str
g = StateGraph(State)
g.add_node("plan", plan_node)
g.add_node("act", act_node)
g.add_edge(START, "plan")
g.add_conditional_edges("act", should_continue, {"more": "act", "done": END})
g.add_edge("plan", "act")
app = g.compile(checkpointer=checkpointer)- Command(update=..., goto=...) lets a node update state and route in one return
- Send fans out to parallel node runs (map-reduce)
- Subgraphs compose teams of agents; each can have its own state
Lesson 3 of 6
Checkpoints, Interrupts, and Human Approval
Compile with a checkpointer and LangGraph saves state at every super-step, keyed by thread_id. That enables resuming after crashes, time travel for debugging, and human-in-the-loop. Calling interrupt(payload) inside a node pauses the graph; you resume by invoking again with Command(resume=value) on the same thread.
def send_email(state):
draft = state["draft"]
decision = interrupt({"action": "send_email", "draft": draft})
if decision != "approve":
return {"status": "rejected"}
email_api.send(draft, idempotency_key=state["run_id"])
return {"status": "sent"}
app.invoke(inputs, {"configurable": {"thread_id": "t1"}}) # pauses
app.invoke(Command(resume="approve"), {"configurable": {"thread_id": "t1"}})Resume reruns the node
On resume, the node starts over from its first line. Anything with side effects before interrupt() runs twice — keep those steps idempotent or move them after the interrupt / into their own node.
Lesson 4 of 6
LangChain create_agent, Middleware, and Deep Agents
LangChain's create_agent builds a tool-calling agent on top of LangGraph, so you get persistence and streaming without writing the graph. Middleware hooks into the loop to add behavior like tool-call limits, summarization of long histories, PII redaction, or human approval before specific tools.
| Layer | Reach for it when |
|---|---|
| create_agent + middleware | Standard tool-calling agent with policies |
| Custom StateGraph | Explicit multi-step control flow, custom state, complex routing |
| Deep Agents | Long-horizon tasks needing planning, subagents, and a filesystem for memory |
| LangSmith | Tracing, datasets, and evals across all of the above |
- MCP adapters let LangGraph agents use the same MCP servers as your IDE
- LangChain publishes official skills you can install into Claude Code or other skills-compatible agents to help you build with it
Lesson 5 of 6
Choosing a Framework (or None)
| Option | Strength | Tradeoff |
|---|---|---|
| Plain SDK calls + your code | Full control, minimal deps | You build persistence, retries, tracing |
| LangGraph | Durable state, HITL, graph control | Learning curve; graph thinking |
| OpenAI Agents SDK / Claude Agent SDK | Tight fit with one provider's agent loop and tools | Portability across providers |
| CrewAI, AutoGen, others | Fast multi-agent prototypes | Less control over state and failure handling |
Decision rule
If you need pause/resume, audit trails, or approvals, pick something with first-class checkpoints. If it is a single tool-calling loop, a thin SDK wrapper may be all you need.
Lesson 6 of 6
Worked Example: The Double Refund
A support agent issues refunds after a human approves them.
def refund_node(state):
payments.refund(state["order_id"]) # side effect
ok = interrupt({"approve": state["order_id"]})
...
# resume → node reruns from the top → customer refunded twicedef approve(state):
decision = interrupt({"order_id": state["order_id"]})
return Command(goto="execute" if decision == "approve" else "explain")
def execute(state):
if state.get("refund_id"): return {}
rid = payments.refund(state["order_id"],
idempotency_key=state["thread_id"])
return {"refund_id": rid}
# test: resume twice → exactly one refund call- Approval moved into its own node with no side effects
- The write happens after approval, with an idempotency key
- A test proves resume safety instead of trusting it
Takeaway
Interrupted nodes rerun. Design for it before it costs real money.