machine learning +
Build a Python AI Chatbot with Memory Using LangChain
LangGraph Nodes, Edges & State: Core Concepts Explained
Understand LangGraph nodes edges state and conditional routing through visual diagrams and runnable Python examples you can copy and extend.
Learn how nodes, edges, conditional edges, and the State object work together in LangGraph — with hands-on code you can run right now.
So you built your first LangGraph graph. It ran. But do you really know what add_node, add_edge, and that TypedDict class do behind the scenes?
Here’s what I see all the time. People copy-paste LangGraph code. It works. Then they try to change something — add a branch, tweak the state, drop in a new step. And they get stuck. Why? They never built a clear picture of how the pieces fit.
I wrote this guide to fix that. By the end, you’ll know what nodes, edges, and state do. You’ll build graphs from scratch, step by step. Not by copying. By truly getting it.
Prerequisites
- Python version: 3.10+
- Required libraries: langgraph (0.3+)
- Install:
pip install langgraph - Previous article: LangGraph Installation, Setup, and Your First Graph — you should be comfortable creating and running a basic graph
- Time to complete: ~25 minutes
What Are Nodes in LangGraph?
Think about making breakfast. You crack the eggs. That’s one task. You whisk them. That’s a second task. You pour the mix into a pan. Task three. Each task takes something in and gives something back.
That’s how LangGraph nodes work. A node is a Python function. It does one job. It reads the current state, does its work, and hands back the changes.
python
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class SimpleState(TypedDict):
message: str
def greet(state: SimpleState) -> dict:
return {"message": "Hello from the node!"}
graph = StateGraph(SimpleState)
graph.add_node("greet", greet)
graph.add_edge(START, "greet")
graph.add_edge("greet", END)
app = graph.compile()
result = app.invoke({"message": ""})
print(result)
python
{'message': 'Hello from the node!'}
See the greet function? That’s the node. It got the state (with a blank message), then sent back a new value. LangGraph took care of the rest — it called the function, fed it the state, and grabbed the result.
Here are three things to note. First, a node is just a plain Python function. No special setup. Second, it takes state in and gives back a dict with updates. Third, you hook it up with add_node("name", function).
Key Insight: A LangGraph node is just a Python function. Give it the state. It does some work. It sends back updates. If you can write a function in Python, you can build a node.
What Can a Node Actually Do?
Whatever Python can do. Call an LLM. Hit a database. Change some text. Fire off an API call. Crunch numbers. There’s just one rule: take state in, send state updates back.
python
def transform_message(state: SimpleState) -> dict:
original = state["message"]
return {"message": original.upper() + " (transformed)"}
graph2 = StateGraph(SimpleState)
graph2.add_node("greet", greet)
graph2.add_node("transform", transform_message)
graph2.add_edge(START, "greet")
graph2.add_edge("greet", "transform")
graph2.add_edge("transform", END)
app2 = graph2.compile()
result2 = app2.invoke({"message": ""})
print(result2)
python
{'message': 'HELLO FROM THE NODE! (transformed)'}
First, greet ran and set the message. Then transform grabbed it, made it uppercase, and tacked on a suffix. One node’s output became the next node’s input.
What Are Edges — And Why Do They Matter?
Nodes on their own? Just loose functions. Nothing links them. Nothing runs.
Edges are the wires. They tell LangGraph the order: “When this node is done, run that one next.” The line graph.add_edge("greet", "transform") means: go to transform right after greet wraps up.
START and END — The Built-In Boundaries
Every graph comes with two special nodes: START and END. You import them from langgraph.graph.
START is where things kick off. Link it to your first node. When you call app.invoke(), LangGraph follows that edge to find what runs first.
END is the stop sign. Once a node links to END, the graph hands back the final state.
python
from langgraph.graph import START, END
# The minimum graph structure:
# START -> your_node -> END
Warning: Forget the edge from `START`, and your graph compiles but does nothing when invoked. Forget the edge to `END`, and LangGraph raises an error — it detects a dead end with no exit path.
Chaining Multiple Nodes
Need three nodes in a row? Add three edges. Each edge is one link in the chain.
Here’s a three-step data pipeline. Each step changes the text field and logs its name to steps_completed:
python
class PipelineState(TypedDict):
text: str
steps_completed: list[str]
def step_one(state: PipelineState) -> dict:
return {
"text": "raw data",
"steps_completed": ["step_one"]
}
def step_two(state: PipelineState) -> dict:
updated = state["text"] + " -> cleaned"
steps = state["steps_completed"] + ["step_two"]
return {"text": updated, "steps_completed": steps}
def step_three(state: PipelineState) -> dict:
updated = state["text"] + " -> analyzed"
steps = state["steps_completed"] + ["step_three"]
return {"text": updated, "steps_completed": steps}
Now wire them up and run:
python
pipeline = StateGraph(PipelineState)
pipeline.add_node("step_one", step_one)
pipeline.add_node("step_two", step_two)
pipeline.add_node("step_three", step_three)
pipeline.add_edge(START, "step_one")
pipeline.add_edge("step_one", "step_two")
pipeline.add_edge("step_two", "step_three")
pipeline.add_edge("step_three", END)
app3 = pipeline.compile()
result3 = app3.invoke({"text": "", "steps_completed": []})
print(f"Final text: {result3['text']}")
print(f"Steps: {result3['steps_completed']}")
python
Final text: raw data -> cleaned -> analyzed
Steps: ['step_one', 'step_two', 'step_three']
Each add_edge call linked one node to the next. The state moved through them in order, picking up changes at each stop.
How Does the State Object Work as Your Graph’s Shared Memory?
Every beginner asks the same thing: “How does Node B find out what Node A did?” The short answer: state.
State is a shared data bucket. Every node reads from it. Every node writes to it. It’s the glue. It’s how nodes talk to each other.
Defining State with TypedDict
You set up state with Python’s TypedDict. This tells LangGraph what fields your data has:
python
from typing import TypedDict
class ChatState(TypedDict):
user_input: str
response: str
turn_count: int
Three fields here: user_input, response, turn_count. Every node in the graph can see and change any of them.
Why use TypedDict instead of a plain dict? Two wins. Your editor gives you autocomplete and flags typos. Also, LangGraph checks your updates at runtime using the type hints.
Tip: Start with only the fields you need. You can add more fields later as your graph grows. Don’t design a 20-field state upfront — let the requirements emerge.
How Does State Flow Through the Graph?
Most guides skip this part. I want to walk you through each step. Once you see the flow, LangGraph clicks.
This graph takes a number, adds 5, doubles it, then takes away 3. Each node logs what it did:
python
class CounterState(TypedDict):
value: int
history: list[str]
def add_five(state: CounterState) -> dict:
new_value = state["value"] + 5
return {
"value": new_value,
"history": state["history"] + [f"add_five: {state['value']} -> {new_value}"]
}
def double_it(state: CounterState) -> dict:
new_value = state["value"] * 2
return {
"value": new_value,
"history": state["history"] + [f"double_it: {state['value']} -> {new_value}"]
}
def subtract_three(state: CounterState) -> dict:
new_value = state["value"] - 3
return {
"value": new_value,
"history": state["history"] + [f"subtract_three: {state['value']} -> {new_value}"]
}
Build it and start with a value of 10:
python
counter_graph = StateGraph(CounterState)
counter_graph.add_node("add_five", add_five)
counter_graph.add_node("double_it", double_it)
counter_graph.add_node("subtract_three", subtract_three)
counter_graph.add_edge(START, "add_five")
counter_graph.add_edge("add_five", "double_it")
counter_graph.add_edge("double_it", "subtract_three")
counter_graph.add_edge("subtract_three", END)
counter_app = counter_graph.compile()
result4 = counter_app.invoke({"value": 10, "history": []})
print(f"Final value: {result4['value']}")
print("\nExecution trace:")
for entry in result4["history"]:
print(f" {entry}")
python
Final value: 27
Execution trace:
add_five: 10 -> 15
double_it: 15 -> 30
subtract_three: 30 -> 27
Let me break it down step by step:
| Step | Node | Input value | What it does | Output value |
|---|---|---|---|---|
| 1 | add_five | 10 | 10 + 5 | 15 |
| 2 | double_it | 15 | 15 * 2 | 30 |
| 3 | subtract_three | 30 | 30 – 3 | 27 |
Each node got the state from the node before it. The value hopped from node to node, changing at each step.
Key Insight: State is not copied between nodes — it’s updated in place. When a node returns `{“value”: 15}`, LangGraph merges that into the existing state. Fields the node doesn’t return stay unchanged.
Partial State Updates — Do You Need to Return Every Field?
No. And this trips people up early on. Just return what you want to change. Leave the rest alone:
python
class ProfileState(TypedDict):
name: str
email: str
verified: bool
def set_name(state: ProfileState) -> dict:
return {"name": "Alice"} # Only updates 'name'
def set_email(state: ProfileState) -> dict:
return {"email": "alice@example.com"} # Only updates 'email'
def verify(state: ProfileState) -> dict:
return {"verified": True} # Only updates 'verified'
python
profile_graph = StateGraph(ProfileState)
profile_graph.add_node("set_name", set_name)
profile_graph.add_node("set_email", set_email)
profile_graph.add_node("verify", verify)
profile_graph.add_edge(START, "set_name")
profile_graph.add_edge("set_name", "set_email")
profile_graph.add_edge("set_email", "verify")
profile_graph.add_edge("verify", END)
profile_app = profile_graph.compile()
result5 = profile_app.invoke({"name": "", "email": "", "verified": False})
print(result5)
python
{'name': 'Alice', 'email': 'alice@example.com', 'verified': True}
Each node changed just one field. LangGraph merged all three updates on its own. The final state shows every change from the whole chain.
How Do Conditional Edges Help Your Graph Make Choices?
Normal edges are like train tracks. Fixed. Set in stone. Conditional edges are like highway ramps — the route depends on what’s going on right now.
This is where LangGraph gets really useful. Your graph looks at the current state and picks which node goes next. The same graph can take a different path each time you run it.
Your First Conditional Edge
You write a routing function. It checks the state and returns the name of the next node. LangGraph calls this function after the source node finishes. Then it follows the path.
Here’s a content sorter. The classify node reads the text and sets a category based on what it finds:
python
class ContentState(TypedDict):
text: str
category: str
result: str
def classify(state: ContentState) -> dict:
text = state["text"].lower()
if "urgent" in text or "emergency" in text:
return {"category": "urgent"}
elif "question" in text or "?" in text:
return {"category": "question"}
else:
return {"category": "general"}
Now the routing function reads the category and picks a handler:
python
def route_content(state: ContentState) -> str:
if state["category"] == "urgent":
return "handle_urgent"
elif state["category"] == "question":
return "handle_question"
else:
return "handle_general"
def handle_urgent(state: ContentState) -> dict:
return {"result": f"URGENT: Escalated '{state['text']}'"}
def handle_question(state: ContentState) -> dict:
return {"result": f"Q&A: Processing question '{state['text']}'"}
def handle_general(state: ContentState) -> dict:
return {"result": f"GENERAL: Filed '{state['text']}'"}
Use add_conditional_edges to wire it up. Pass in the source node, the routing function, and a mapping of return values to node names:
python
content_graph = StateGraph(ContentState)
content_graph.add_node("classify", classify)
content_graph.add_node("handle_urgent", handle_urgent)
content_graph.add_node("handle_question", handle_question)
content_graph.add_node("handle_general", handle_general)
content_graph.add_edge(START, "classify")
content_graph.add_conditional_edges(
"classify",
route_content,
{
"handle_urgent": "handle_urgent",
"handle_question": "handle_question",
"handle_general": "handle_general",
}
)
content_graph.add_edge("handle_urgent", END)
content_graph.add_edge("handle_question", END)
content_graph.add_edge("handle_general", END)
content_app = content_graph.compile()
Let’s test all three paths:
python
test_inputs = [
{"text": "Emergency! Server is down!", "category": "", "result": ""},
{"text": "What is LangGraph?", "category": "", "result": ""},
{"text": "Weekly status update", "category": "", "result": ""},
]
for inp in test_inputs:
output = content_app.invoke(inp)
print(f"Input: '{inp['text']}'")
print(f" Category: {output['category']}, Result: {output['result']}")
python
Input: 'Emergency! Server is down!'
Category: urgent, Result: URGENT: Escalated 'Emergency! Server is down!'
Input: 'What is LangGraph?'
Category: question, Result: Q&A: Processing question 'What is LangGraph?'
Input: 'Weekly status update'
Category: general, Result: GENERAL: Filed 'Weekly status update'
One graph. Three paths. The routing function looked at state["category"] and chose the right handler every time.
Note: The routing function must return a string that matches a key in the mapping dictionary (or a registered node name if you skip the mapping). Returning an unrecognized string raises a `ValueError`.
Breaking Down add_conditional_edges
This method takes three parts. Here’s what each one does:
python
content_graph.add_conditional_edges(
"classify", # Source node — branch FROM here
route_content, # Routing function — decides WHERE next
{ # Mapping — translates return values to node names
"handle_urgent": "handle_urgent",
"handle_question": "handle_question",
"handle_general": "handle_general",
}
)
The mapping is optional. If your routing function already returns real node names, leave it out:
python
# No mapping needed — route_content already returns node names
content_graph.add_conditional_edges("classify", route_content)
I still like to include the mapping. It shows all the paths in one spot. Makes the graph easier to read at a glance.
How Can You See Your Graph’s Shape?
Don’t guess if your graph is wired right. Look at it.
LangGraph’s get_graph() method gives you the graph shape. Then draw_mermaid() turns it into a diagram you can view in any Mermaid tool (GitHub, VS Code, mermaid.live):
python
mermaid_code = content_app.get_graph().draw_mermaid()
print(mermaid_code)
python
%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>])
classify(classify)
handle_urgent(handle_urgent)
handle_question(handle_question)
handle_general(handle_general)
__end__([<p>__end__</p>])
__start__ --> classify;
classify -.-> handle_urgent;
classify -.-> handle_question;
classify -.-> handle_general;
handle_urgent --> __end__;
handle_question --> __end__;
handle_general --> __end__;
Solid arrows (-->) mean normal edges. Dashed arrows (.->) mean conditional edges. Paste this into a Mermaid tool and you’ll see the full layout.
Tip: Make visualization a habit. Every time you build or modify a graph, print the Mermaid output. You’ll catch wiring mistakes before they cause confusing runtime errors.
How Do Flowcharts Map to LangGraph?
If you’ve drawn a flowchart before, you already get the LangGraph model. Here’s a side-by-side look:
| Flowchart Element | LangGraph Equivalent | Code |
|---|---|---|
| Process box (rectangle) | Node | add_node("name", func) |
| Arrow | Edge | add_edge("a", "b") |
| Diamond (decision) | Conditional edge | add_conditional_edges(...) |
| Start oval | START | add_edge(START, "first") |
| End oval | END | add_edge("last", END) |
| Data on arrows | State (TypedDict) | class MyState(TypedDict) |
The big gap? A flowchart sits on paper. A LangGraph graph runs as code. Your boxes become functions. Your arrows become edges. Your diamonds become conditional branches.
How Does It All Come Together? — Order Processing System
Let’s mix nodes, edges, conditional edges, and state into something you’d see in the real world. This pipeline checks orders, looks up stock, works out the price, and then approves or rejects.
The state holds everything the pipeline needs:
python
class OrderState(TypedDict):
order_id: str
item: str
quantity: int
price_per_unit: float
total: float
in_stock: bool
is_valid: bool
status: str
log: list[str]
Each step is its own node. First, the validator checks if the order makes sense:
python
def validate_order(state: OrderState) -> dict:
errors = []
if state["quantity"] <= 0:
errors.append("Quantity must be positive")
if not state["item"]:
errors.append("Item name required")
is_valid = len(errors) == 0
msg = "Validated OK" if is_valid else f"Validation failed: {errors}"
return {
"is_valid": is_valid,
"log": state["log"] + [f"validate: {msg}"]
}
Next, the stock checker looks up what’s on hand. In a real app, this would call a database:
python
INVENTORY = {"widget": 100, "gadget": 50, "doohickey": 0}
def check_inventory(state: OrderState) -> dict:
stock = INVENTORY.get(state["item"].lower(), 0)
in_stock = stock >= state["quantity"]
return {
"in_stock": in_stock,
"log": state["log"] + [
f"inventory: {state['item']} has {stock} units, need {state['quantity']}"
]
}
The price step adds a 10% bulk cut for orders of 10 or more:
python
def calculate_price(state: OrderState) -> dict:
total = state["quantity"] * state["price_per_unit"]
if state["quantity"] >= 10:
total *= 0.9 # 10% bulk discount
return {
"total": total,
"log": state["log"] + [f"pricing: total=${total:.2f}"]
}
Then come the approve and reject handlers:
python
def approve_order(state: OrderState) -> dict:
return {
"status": "approved",
"log": state["log"] + [f"APPROVED: Order {state['order_id']}"]
}
def reject_order(state: OrderState) -> dict:
return {
"status": "rejected",
"log": state["log"] + [f"REJECTED: Order {state['order_id']}"]
}
The routing function checks both the validation flag and the stock flag:
python
def should_approve(state: OrderState) -> str:
if state["is_valid"] and state["in_stock"]:
return "approve"
return "reject"
Now wire it all up — a straight line through validate, stock, and price, then a fork for the final call:
python
order_graph = StateGraph(OrderState)
order_graph.add_node("validate", validate_order)
order_graph.add_node("check_inventory", check_inventory)
order_graph.add_node("calculate_price", calculate_price)
order_graph.add_node("approve", approve_order)
order_graph.add_node("reject", reject_order)
order_graph.add_edge(START, "validate")
order_graph.add_edge("validate", "check_inventory")
order_graph.add_edge("check_inventory", "calculate_price")
order_graph.add_conditional_edges(
"calculate_price",
should_approve,
{"approve": "approve", "reject": "reject"}
)
order_graph.add_edge("approve", END)
order_graph.add_edge("reject", END)
order_app = order_graph.compile()
Try a good order first — 5 widgets at $29.99 each:
python
good_order = {
"order_id": "ORD-001", "item": "widget",
"quantity": 5, "price_per_unit": 29.99,
"total": 0.0, "in_stock": False,
"is_valid": False, "status": "", "log": []
}
result_good = order_app.invoke(good_order)
print(f"Status: {result_good['status']}")
print(f"Total: ${result_good['total']:.2f}")
for entry in result_good["log"]:
print(f" {entry}")
python
Status: approved
Total: $149.95
validate: Validated OK
inventory: widget has 100 units, need 5
pricing: total=$149.95
APPROVED: Order ORD-001
Now try an item with zero stock:
python
bad_order = {
"order_id": "ORD-002", "item": "doohickey",
"quantity": 5, "price_per_unit": 9.99,
"total": 0.0, "in_stock": False,
"is_valid": False, "status": "", "log": []
}
result_bad = order_app.invoke(bad_order)
print(f"Status: {result_bad['status']}")
for entry in result_bad["log"]:
print(f" {entry}")
python
Status: rejected
validate: Validated OK
inventory: doohickey has 0 units, need 5
pricing: total=$49.95
REJECTED: Order ORD-002
Same graph. Different result. The routing function saw in_stock=False and sent the order to the reject path.
What Are the Most Common Mistakes — And How Do You Fix Them?
Mistake 1: Orphan Nodes — Added but Never Linked
python
# WRONG — orphan node never executes
graph = StateGraph(SimpleState)
graph.add_node("greet", greet)
graph.add_node("orphan", transform_message) # No edges!
graph.add_edge(START, "greet")
graph.add_edge("greet", END)
LangGraph won’t flag this when you compile. The orphan node sits in the graph but never runs. Always check with get_graph() that every node has edges going in and out.
Mistake 2: Sending Back Keys That Don’t Match the State
python
class MyState(TypedDict):
count: int
def bad_node(state: MyState) -> dict:
return {"counter": 10} # Typo! 'counter' not in MyState
LangGraph throws an InvalidUpdateError. The key counter doesn’t exist in the TypedDict. Make sure every key you return lines up with a field in your state.
Mistake 3: Routing to a Node That Doesn’t Exist
python
def bad_router(state):
return "process_data" # But you named the node "process"!
The router said "process_data". The node was called "process". LangGraph throws a ValueError. Check that your routing return values match the exact names from add_node.
Warning: These three mistakes account for most debugging time with LangGraph. Before running: (1) verify every node has edges via `get_graph()`, (2) match return dict keys to TypedDict fields, (3) confirm routing return values match node names.
Quick Check — Can You Predict the Output?
Look at this graph. What will x be at the end?
python
class QuizState(TypedDict):
x: int
def add_ten(state: QuizState) -> dict:
return {"x": state["x"] + 10}
def halve(state: QuizState) -> dict:
return {"x": state["x"] // 2}
quiz_graph = StateGraph(QuizState)
quiz_graph.add_node("add_ten", add_ten)
quiz_graph.add_node("halve", halve)
quiz_graph.add_edge(START, "add_ten")
quiz_graph.add_edge("add_ten", "halve")
quiz_graph.add_edge("halve", END)
quiz_app = quiz_graph.compile()
# What does quiz_app.invoke({"x": 6}) return?
Here’s how it works. x starts at 6. add_ten brings it to 16. halve cuts it to 8. So the answer is {"x": 8}.
python
result_quiz = quiz_app.invoke({"x": 6})
print(result_quiz)
python
{'x': 8}
Did you get it right? Great — you understand state flow. Got it wrong? Go back and re-read the trace table in the “How Does State Flow” section.
Exercise 1: Build a Temperature Classifier
Instructions: Build a LangGraph graph that classifies a temperature reading as “cold” (below 15), “moderate” (15-30), or “hot” (above 30). Create a classify node that sets the label, three handler nodes that each set an appropriate action string, and a routing function that directs traffic. The TempState TypedDict is provided.
python
# Starter code
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class TempState(TypedDict):
temperature: int
label: str
action: str
# TODO: Write the classify node
def classify(state: TempState) -> dict:
pass
# TODO: Write handler nodes
def handle_cold(state: TempState) -> dict:
pass
def handle_moderate(state: TempState) -> dict:
pass
def handle_hot(state: TempState) -> dict:
pass
# TODO: Write routing function and build graph
Test your solution:
python
# Should print: cold / Turn on heater
result = app.invoke({"temperature": 5, "label": "", "action": ""})
print(result["label"], "/", result["action"])
# Should print: hot / Turn on AC
result = app.invoke({"temperature": 35, "label": "", "action": ""})
print(result["label"], "/", result["action"])
Hints:
1. The classify node checks state["temperature"] against 15 and 30, then returns {"label": "cold"}, {"label": "moderate"}, or {"label": "hot"}.
2. The routing function reads state["label"] and returns the matching handler name (e.g., "handle_cold"). Wire it with add_conditional_edges after the classify node.
Exercise 2: Build a Text Processing Pipeline
Instructions: Build a three-node pipeline that: (1) clean — strips whitespace and lowercases the text, (2) count_words — counts words and stores the count, (3) summarize — creates a summary string. Each node should append its name to the steps list.
python
# Starter code
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class TextState(TypedDict):
text: str
word_count: int
summary: str
steps: list[str]
# TODO: Write clean, count_words, summarize nodes
# TODO: Build the graph
Test: app.invoke({"text": " Hello World From Python ", ...}) should produce text="hello world from python", word_count=4, summary="Processed: 4 words", steps=['clean', 'count_words', 'summarize'].
Hints:
1. Use state["text"].strip().lower() in the clean node. Return both the cleaned text and the updated steps list.
2. Use len(state["text"].split()) for word counting. Append each node’s name to steps via state["steps"] + ["node_name"].
Summary
You now know the three core parts of every LangGraph app:
Nodes are Python functions. Each one reads state, does work, and sends back updates. You add them with graph.add_node("name", function).
Edges link nodes and set the run order. Use add_edge for fixed paths. Use add_conditional_edges when you need the graph to pick a path based on state.
State is a TypedDict that acts as shared memory for the whole graph. Every node reads from it. Every node writes to it. You only need to return the fields you change — LangGraph merges the rest.
The built-in START and END nodes mark where the graph begins and ends. Every graph needs at least one path from START to END.
Practice exercise: Build a quiz grading system. The state holds score (int), grade (str), and feedback (str). A calculate_grade node sets the grade based on score (90+=”A”, 80+=”B”, 70+=”C”, below 70=”F”). Use conditional edges to route to different feedback nodes per grade. Test with scores of 95, 85, 72, and 55.
Up next: we’ll dig deeper into state management — reducers, message history, and handling state that grows as data piles up across many nodes.
FAQ
Can a node have more than one normal outgoing edge?
No. A node can have only one normal outgoing edge. If you need branching, use add_conditional_edges with a routing function. That’s the whole reason conditional edges exist.
python
# This raises an error:
# graph.add_edge("my_node", "node_a")
# graph.add_edge("my_node", "node_b") # Error! Already has outgoing edge.
# Use conditional edges for branching
graph.add_conditional_edges("my_node", routing_function)
Can I use Pydantic instead of TypedDict for state?
Yes. LangGraph works with both TypedDict and Pydantic BaseModel. Pydantic adds checks at runtime — if a node returns bad data, you get a clear error right away. For basic graphs, TypedDict does the job. For production apps where you want strict data rules, Pydantic is a solid pick.
What happens when two nodes write to the same state field?
The last node wins. Say node_a sets {"count": 5} and then node_b sets {"count": 10}. The final state has count: 10. If you want to stack values up instead of replacing them, LangGraph has reducer functions. I cover those in the next article on state management.
How many nodes can a graph hold?
There’s no hard cap. Graphs with 30+ nodes run fine in production. But once you pass 10-15 nodes, think about splitting into subgraphs. They’re easier to test and maintain. I cover subgraphs later in this learning path.
Is the run order always the same?
For straight chains (A -> B -> C), yes. For conditional edges, the path depends on state at runtime. But the routing function always gives the same answer for the same input. LangGraph adds no randomness to the order.
References
- LangGraph documentation — Graph API overview. Link
- LangGraph documentation — Concepts: Nodes. Link
- LangGraph documentation — Concepts: Edges. Link
- LangGraph documentation — Concepts: State. Link
- LangGraph documentation — Quickstart. Link
- LangGraph Cheatsheet — Core Concepts. Link
- Python documentation — TypedDict. Link
- LangGraph GitHub repository. Link
Last reviewed: March 2026 | LangGraph version: 0.3+
Free Course
Master Core Python — Your First Step into AI/ML
Build a strong Python foundation with hands-on exercises designed for aspiring Data Scientists and AI/ML Engineers.
Start Free Course →Trusted by 50,000+ learners
Related Course
Master Gen AI — Hands-On
Join 5,000+ students at edu.machinelearningplus.com
Explore Course
Up Next in Learning Path
LangGraph State Management: TypedDict & Reducers
