TechByteByByte

START and END: Why Explicit Entry and Exit Points Matter

Every graph needs a genuine, unambiguous place to start and real ways to actually finish. See multiple termination routes, conditional entry, and what really happens when a graph accidentally has no way out.

#LangGraph#START#END#Recursion

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

  • START and END are real, specific objects — not just naming conventions — marking a graph’s genuine entry point and genuine termination points.
  • START can 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 END triggers the same real GraphRecursionError you already met through create_agent in 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