Draw the Agent Loop: Porting Our Agent to LangGraph
Draw the Agent Loop: Porting Our Agent to LangGraph
Episode #3 of Slowly ChangingHarness .... #edition53
Hey, I'm Prashanth. I'm a data engineer who has spent years in the plumbing of AI systems, and every week I share what I'm actually building, learning, and thinking about. No hype, no fluff. Just the foundations that make AI work in the real world.
Slowly Changing Harness is a series about the engineering layer that wraps an AI agent. Dozens of agent demos get built every week. Very few make it to production. The gap between the two is not the model -- it is the harness. The access controls, the tool boundaries, the resumability, the observability. None of that comes for free. It has to be engineered.
This series covers the fundamental layers of that harness, one episode at a time, using the same running example throughout. The goal is not to teach you how to build a stock market agent. It is to teach you what production agents actually need -- so you can apply it to whatever you are building.
Start with the Series Opener followed by the Episodes.
Draw The Loop- This Episode
Full read: ~7 min · Scan: 90 sec
Subscribe for free to receive new posts and support my work.
Before I present about Agents, I always draw a picture for my audience
A few boxes, a few arrows.
I do this because the concept of agents seems whimsical and overwhelming, but when you draw the loop, you see clearly that it is nothing but engineering. The same thing we have been learning for the past 20 years, but now an LLM is a part of it.
So when I stumbled upon LangGraph, a framework designed around a graph to orchestrate that exact loop, I was immediately hooked.
It is not only intuitive -- it is the one framework that helps you understand the harness at a deeper level. It is also the most used framework in the industry right now. That is why we are switching starting this episode. In LangGraph, the diagram is not documentation -- it is the program. Every box becomes a node. Every arrow becomes an edge. The graph you draw on paper and the graph you compile are the same thing.
Draw it first
Here is the loop we are building. As an exercise draw it by hand on a piece of paper and do it before you proceed to read the code.
That is our simplest Agent loop just an LLM that can call tools.
LangGraph CrashCourse - 3 Concepts
LangGraph maps directly to that diagram. Three things to know before you read any LangGraph code.
State flows along the arrows. It is a typed dictionary that every node reads and writes back to. In our agent, state is the message history -- the full conversation between the user, the LLM, and the tools. It is good to remember that state is immutable, which means every node operates on the state and returns a new state.
Nodes are the boxes. Each node is a plain Python function: take state in, return a new state out. The LLM call is a node. The tool execution is a node.
Edges are the arrows. A regular edge always goes A to B. A conditional edge looks at the current state and decides which node comes next for example, a tool call.
That is all you need to know for it to click. Less is more. Time to Build!
The agent state
from typing import TypedDict, Annotated
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
add_messages is a built-in reducer. When a node returns new messages, LangGraph appends them to the existing list rather than replacing it. The full conversation accumulates in state as the graph runs.
The nodes (the boxes)
search_web, read_url, and write_to_sheet are our core tools unchanged from Episode 1. Full implementation in the repo.
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import ToolNode
from shared_tools import search_web, read_url, write_to_sheet
model = ChatOpenAI(model="gpt-4o-mini")
model_with_tools = model.bind_tools([search_web, read_url, write_to_sheet])
# Box 1: call the LLM
def llm_node(state: AgentState):
response = model_with_tools.invoke(state["messages"])
return {"messages": [response]}
# Box 2: execute whatever tool the LLM called
tools = ToolNode([search_web, read_url, write_to_sheet])
ToolNode takes a list of tools and handles execution, result formatting, and ToolMessage construction internally. Box 2 is one line.
Wiring the graph (drawing the arrows in code)
from langgraph.graph import StateGraph, START
from langgraph.prebuilt import tools_condition
graph = StateGraph(AgentState)
graph.add_node("llm", llm_node)
graph.add_node("tools", tools)
graph.add_edge(START, "llm")
graph.add_conditional_edges("llm", tools_condition) # has tool calls? → tools, else → END
graph.add_edge("tools", "llm") # loop back after every tool run
app = graph.compile()
Compare this to the diagram. add_node is a box. add_edge is an arrow. add_conditional_edges with tools_condition is the branching arrow -- a built-in LangGraph function that checks whether the last message has tool calls and routes accordingly.
That is the complete agent. Same tools.
Now call:
print(app.get_graph().draw_mermaid())Look at the sheet of paper you used, that is the diagram you drew at the top of this article. LangGraph generated it from the compiled graph. The drawing and the program are the same thing.
Running it
result = app.invoke({
"messages": [
(
"system",
"You are a stock research agent. Use search_web and read_url to gather data. "
"Once you have researched all stocks, call write_to_sheet once with all rows as a batch.",
),
(
"user",
"Research 5 high-growth tech stocks. For each: ticker, current price, P/E ratio, "
"trailing 12M revenue in millions, one-line analyst view. Write all findings to the tracker sheet.",
),
]
})
print(result["messages"][-1].content)
Same agent as Episode 1. Now every step has a name.
What you gain
The loop is visible. Every step is named. Every transition is explicit. When something breaks in production you know exactly which node failed -- not “something in the loop failed somewhere.”
You drew it before you wrote it. Call app.get_graph().draw_mermaid() and verify the code matches the diagram. The diagram and the program stay in sync automatically.
Adding a step is adding a node. An audit log, a retry handler, a second model as a fallback -- each is a node. Wire it in with add_node and add_edge. The rest of the graph does not change.
What you give up
More setup. The LangGraph version is longer than the Pydantic AI version for the same behaviour. For a simple research agent, that cost is real -- weigh it against what you need.
The abstractions are gone. Pydantic AI handled tool dispatch, message formatting, and the loop. In LangGraph you wire all of it explicitly. None of it is hard but all of it is yours to get right.
Try this
Run the agent and watch the messages accumulate. After the run, inspect the full message history:
for msg in result["messages"]:
print(type(msg).__name__, ":", str(msg.content)[:80])
Every tool call, every result, every LLM response is there in order.
What is next
The agent now runs as an explicit, visible graph. You drew it, you compiled it, and draw_mermaid() confirms the code matches the drawing.
Next we look at Durable Agents, how ChatGPT and Claude remember what you said months ago. Let’s find out together in the next episode.
💬 Before you go
What does the loop look like for your use case? Draw it. Reply and show me..
While you are at it! Go build this agent but for your own simple use case, but remember to draw the loop before you start coding.
📩 If this is useful, forward it to an engineer who is tired of chasing the next AI buzzword, or [subscribe →] if someone forwarded this to you.
Stay building,
Prashanth
Substack | LinkedIn | Previous editions
Thanks for reading Urban Data Engineer's Newsletter! Subscribe for free to receive new posts and support my work.
Urban Data Engineer's Newsletter is free today. But if you enjoyed this post, you can tell Urban Data Engineer's Newsletter that their writing is valuable by pledging a future subscription. You won't be charged unless they enable payments.
© 2026 Prashanth Xavier
548 Market Street PMB 72296, San Francisco, CA 94104