You’ve already used START and END in every single example so far, without this course ever stopping to explain them properly. That was deliberate — they needed to feel completely ordinary before this module asked you to actually think about them. Now it’s worth stopping, because these two objects are doing more real, structural work than their plain names suggest.
What START and END actually are
START and END aren’t just conventions or placeholder names — they’re real, specific objects LangGraph provides, imported directly from langgraph.graph. START marks the genuine, single entry point every invocation of a graph begins from. END marks a genuine, real termination — reaching it means this particular run of the graph is completely finished, and no further nodes will execute.
from langgraph.graph import StateGraph, START, END
You’ve imported this line in every module so far. It’s worth knowing, now, exactly what these two names actually guarantee.
Why an explicit entry point is worth having at all
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
message: str
def process(state: State) -> dict:
return {"message": f"Processed: {state['message']}"}
builder = StateGraph(State)
builder.add_node("process", process)
builder.add_edge(START, "process")
builder.add_edge("process", END)
graph = builder.compile()
print(graph.invoke({"message": "hello"}))
builder.add_edge(START, "process") is a real, deliberate statement: this graph has exactly one place execution genuinely begins. Without it, LangGraph has no way of knowing which node should run first — and it won’t guess. compile() will genuinely fail with a clear error if no edge from START exists, rather than silently picking an arbitrary node to start from.
Conditional entry — routing immediately, based on the very first input
START doesn’t have to lead to just one fixed node. You can route immediately, based on what the graph was given, before any real work has happened at all.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
request_type: str
result: str
def handle_billing(state: State) -> dict:
return {"result": "Routed to billing."}
def handle_technical(state: State) -> dict:
return {"result": "Routed to technical support."}
def choose_entry(state: State) -> str:
return "handle_billing" if state["request_type"] == "billing" else "handle_technical"
builder = StateGraph(State)
builder.add_node("handle_billing", handle_billing)
builder.add_node("handle_technical", handle_technical)
builder.add_conditional_edges(START, choose_entry, ["handle_billing", "handle_technical"])
builder.add_edge("handle_billing", END)
builder.add_edge("handle_technical", END)
graph = builder.compile()
print(graph.invoke({"request_type": "billing", "result": ""}))
print(graph.invoke({"request_type": "technical", "result": ""}))
Notice add_conditional_edges(START, ...) — the exact same routing mechanism you’ll learn fully in the next module, applied directly to the graph’s very first decision. This is genuinely useful when a workflow’s entire shape depends on what kind of request it’s actually handling from the start, rather than every request passing through one identical, shared first node.
Multiple, genuinely different termination routes
Just as a graph can have several possible entry paths, it can have several, entirely different points that all legitimately reach END.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
refund_amount: float
outcome: str
def check_amount(state: State) -> dict:
return {}
def auto_approve(state: State) -> dict:
return {"outcome": "Automatically approved — under $50."}
def needs_review(state: State) -> dict:
return {"outcome": "Flagged for manual review — over $50."}
def route(state: State) -> str:
return "auto_approve" if state["refund_amount"] < 50 else "needs_review"
builder = StateGraph(State)
builder.add_node("check_amount", check_amount)
builder.add_node("auto_approve", auto_approve)
builder.add_node("needs_review", needs_review)
builder.add_edge(START, "check_amount")
builder.add_conditional_edges("check_amount", route, ["auto_approve", "needs_review"])
builder.add_edge("auto_approve", END)
builder.add_edge("needs_review", END)
graph = builder.compile()
print(graph.invoke({"refund_amount": 20.0, "outcome": ""}))
print(graph.invoke({"refund_amount": 200.0, "outcome": ""}))
Both auto_approve and needs_review genuinely, legitimately reach END — there’s no requirement that a graph converge back to one single, shared final node before finishing. Different real outcomes are allowed to end the workflow in different, equally valid places.
flowchart TD
START --> check_amount
check_amount -->|under $50| auto_approve --> END1[END]
check_amount -->|over $50| needs_review --> END2[END]
What actually happens when a graph has no real way to reach END
This is worth seeing directly, not just being warned about abstractly.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
count: int
def increment(state: State) -> dict:
return {"count": state["count"] + 1}
def always_continue(state: State) -> str:
return "increment" # this ALWAYS routes back to increment — no exit exists
builder = StateGraph(State)
builder.add_node("increment", increment)
builder.add_conditional_edges("increment", always_continue, ["increment"])
builder.add_edge(START, "increment")
graph = builder.compile()
try:
graph.invoke({"count": 0})
except Exception as e:
print(f"Failed: {type(e).__name__}: {e}")
Run this, and you’ll hit a real, genuine GraphRecursionError — the exact same exception you already met in your LangChain course, when create_agent’s own loop exceeded its recursion_limit. This isn’t a coincidence: create_agent is built on this same underlying graph engine, and this is precisely where that protection actually lives. LangGraph’s own default recursion limit exists specifically to catch exactly this mistake — a routing function with genuinely no path to END — and stop it before it runs forever, rather than silently consuming real API calls and real money indefinitely.
Common mistakes worth avoiding
Forgetting an edge to END from a node that’s meant to be a real, valid stopping point. A node with no outgoing edge at all — not even one to END — will genuinely cause compile() to fail, since LangGraph has no way of knowing what should happen once that node finishes.
Writing a conditional routing function that can return a value with no matching edge. If route() in a conditional edge can theoretically return a string that wasn’t included in the list passed to add_conditional_edges(...), that specific, real execution will fail the moment it actually happens — often only under an input combination nobody happened to test.
Assuming the default recursion limit will always save you from genuinely intended, deliberate loops. Recall the next module’s real review-and-revise loops — those are meant to run several genuine iterations before reaching END. The default limit exists to catch accidental infinite loops, not to arbitrarily cap every legitimate, bounded loop you deliberately design; you’ll set an appropriate, deliberate limit for those cases directly.
What you should take away from this module
STARTandENDare real, specific objects — not just naming conventions — marking a graph’s genuine entry point and genuine termination points.STARTcan route conditionally, exactly like any other conditional edge, letting a workflow’s shape depend on its input from the very first step.- A graph can have several, entirely different nodes that all legitimately reach
END— convergence to one shared final node is never required. - A routing function with no genuine path to
ENDtriggers the same realGraphRecursionErroryou already met throughcreate_agentin your LangChain course — this is exactly where that protection actually lives.
Where this goes next
You now understand every piece needed to build a real, complete graph. The next module puts all of it together — state, nodes, edges, START, END — building your first complete graph from scratch, then genuinely inspecting what its final state actually looks like.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed