You now know every individual piece — state in Module 3, nodes in Module 4, edges in Module 5, START and END in Module 6. This module doesn’t teach anything new. It’s where you actually put them together, deliberately starting as small as possible, and growing the graph one genuine step at a time.
The smallest possible graph
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
name: str
greeting: str
def greet(state: State) -> dict:
return {"greeting": f"Hello, {state['name']}!"}
builder = StateGraph(State)
builder.add_node("greet", greet)
builder.add_edge(START, "greet")
builder.add_edge("greet", END)
graph = builder.compile()
result = graph.invoke({"name": "Amara", "greeting": ""})
print(result)
START → greet → END. Nothing about this should feel unfamiliar — this is genuinely the exact same shape from every previous module’s examples, just isolated on its own, as the true minimum viable graph.
Adding a second node
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
name: str
greeting: str
farewell: str
def greet(state: State) -> dict:
return {"greeting": f"Hello, {state['name']}!"}
def say_goodbye(state: State) -> dict:
return {"farewell": f"Goodbye, {state['name']}. Take care!"}
builder = StateGraph(State)
builder.add_node("greet", greet)
builder.add_node("say_goodbye", say_goodbye)
builder.add_edge(START, "greet")
builder.add_edge("greet", "say_goodbye")
builder.add_edge("say_goodbye", END)
graph = builder.compile()
print(graph.invoke({"name": "Amara", "greeting": "", "farewell": ""}))
One new node, one new edge — greet now leads to say_goodbye instead of directly to END. Notice the state schema grew too, with farewell added alongside the existing fields, exactly the incremental growth pattern real workflows actually go through.
Inspecting the final state properly
So far, every example has just print(result). Let’s actually look at what that result genuinely contains, field by field, rather than treating it as an opaque blob.
result = graph.invoke({"name": "Amara", "greeting": "", "farewell": ""})
print("Full state:", result)
print("Name:", result["name"])
print("Greeting:", result["greeting"])
print("Farewell:", result["farewell"])
print("Type of result:", type(result))
result is a genuine, plain Python dictionary — every field from your State schema, in its final, fully-updated form after every node has run. This matters more than it might seem: real debugging almost always starts with looking at exactly this — what does the final state actually contain, and does it match what you expected at each field, individually.
A first, real version of the Customer Resolution Agent
Let’s build something closer to the recurring application this course will keep returning to — deliberately small, its very first stage.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langchain.chat_models import init_chat_model
class State(TypedDict):
user_query: str
understanding: str
model = init_chat_model("openai:gpt-4o-mini")
def understand(state: State) -> dict:
response = model.invoke(f"In one sentence, summarize what this customer needs: {state['user_query']}")
return {"understanding": response.content}
builder = StateGraph(State)
builder.add_node("understand", understand)
builder.add_edge(START, "understand")
builder.add_edge("understand", END)
graph = builder.compile()
result = graph.invoke({"user_query": "My package says delivered but I never got it, and I need this resolved today.", "understanding": ""})
print(result["understanding"])
This is genuinely Stage 1 of the application you’ll keep extending, module after module, for the rest of this course — START → understand → END. Every future stage adds real, deliberate capability on top of this exact same foundation, never replacing it.
Common mistakes worth avoiding
Writing a large, multi-node graph in one pass and debugging it as a whole. Recall this module’s own deliberate approach — build one node, verify it genuinely works, then add the next. A graph that fails after ten nodes were all added at once gives you almost no information about which one is actually responsible.
Forgetting to initialize every field the state schema declares. Every graph.invoke({...}) call in this module explicitly included every field, even ones a node would set — "greeting": "", for instance. Omitting a declared field entirely can cause a real KeyError the moment a node tries to read it before anything has written to it.
What you should take away from this module
- Building a graph is genuinely incremental — start with one node, verify it works, then add the next piece deliberately, rather than writing a large graph all at once and debugging it as a whole.
- A graph’s final result is a plain, real dictionary — inspect it field by field when debugging, not as one undifferentiated blob.
- The Customer Resolution Agent’s first stage,
START → understand → END, is deliberately this simple — every later module in this course adds one genuine, real capability on top of it.
Where this goes next
The next module is a major one: Conditional Edges, covering five genuinely real routing scenarios — model selection, support routing, RAG-vs-direct, tool selection, and human escalation — where the path a workflow actually takes depends on what’s really in its state.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed