TechByteByByte

Edges: Connecting Nodes Into a Workflow

Nodes do the work; edges decide what happens next. Build real sequential chains, understand fan-in, and get a clear roadmap of the three tiers of routing this course covers.

#LangGraph#Edges#Workflow Design

Recall Module 4’s honest definition — a node reads state and returns an update. On its own, a node genuinely doesn’t know what happens after it finishes. That’s not an oversight; it’s deliberate. Deciding “what runs next” is a completely separate concern, and it belongs to edges. Keeping these two things cleanly separate — what work happens, versus what happens after — is exactly what makes a graph genuinely easier to reason about than a tangle of nested if statements.

The simplest possible connection: A → B

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    text: str

def add_greeting(state: State) -> dict:
    return {"text": f"Hello! {state['text']}"}

def add_signature(state: State) -> dict:
    return {"text": f"{state['text']} — Sent by the resolution bot."}

builder = StateGraph(State)
builder.add_node("add_greeting", add_greeting)
builder.add_node("add_signature", add_signature)

builder.add_edge(START, "add_greeting")
builder.add_edge("add_greeting", "add_signature")
builder.add_edge("add_signature", END)

graph = builder.compile()
print(graph.invoke({"text": "your refund has been processed."}))

builder.add_edge("add_greeting", "add_signature") is the entire idea. It’s a fixed, unconditional statement: once add_greeting finishes, add_signature runs next, every single time, with no decision involved at all.

Extending to A → B → C, with a real, three-step shape

Let’s build something closer to a genuine workflow — classify, retrieve, respond, chained together in sequence.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    user_query: str
    category: str
    context: str
    answer: str

def classify(state: State) -> dict:
    return {"category": "billing" if "refund" in state["user_query"].lower() else "general"}

def retrieve(state: State) -> dict:
    return {"context": "Refunds take 5-7 business days."} if state["category"] == "billing" else {"context": ""}

def respond(state: State) -> dict:
    return {"answer": f"Based on our policy: {state['context']}" if state["context"] else "How can I help?"}

builder = StateGraph(State)
builder.add_node("classify", classify)
builder.add_node("retrieve", retrieve)
builder.add_node("respond", respond)

builder.add_edge(START, "classify")
builder.add_edge("classify", "retrieve")
builder.add_edge("retrieve", "respond")
builder.add_edge("respond", END)

graph = builder.compile()
print(graph.invoke({"user_query": "When will my refund arrive?", "category": "", "context": "", "answer": ""}))

Notice this three-node chain is a real, working instance of a pattern you’ve already studied conceptually in your Agent Design Patterns course: prompt chaining — breaking one task into a fixed sequence of smaller, focused steps, each one handing its output to the next. This is worth naming directly, because it’s the first of several moments in this course where a design pattern you already know by name turns out to be exactly this simple to actually implement.

Multiple edges into the same node — fan-in

An edge doesn’t have to be one-to-one. Several different nodes can all point to the same next node.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    source: str
    log: str

def from_email(state: State) -> dict:
    return {"source": "email"}

def from_chat(state: State) -> dict:
    return {"source": "chat"}

def log_request(state: State) -> dict:
    return {"log": f"Request received via {state['source']}"}

builder = StateGraph(State)
builder.add_node("from_email", from_email)
builder.add_node("from_chat", from_chat)
builder.add_node("log_request", log_request)

builder.add_edge(START, "from_email")
builder.add_edge("from_email", "log_request")
builder.add_edge("log_request", END)

graph = builder.compile()
print(graph.invoke({"source": "", "log": ""}))

While this specific example only exercises one path, notice that log_request genuinely could receive an edge from from_chat too — builder.add_edge("from_chat", "log_request") — and both paths would converge on the same shared node. This convergence pattern, several different starting points genuinely funneling into one common step, is a real, frequently used shape you’ll see again once parallel execution is covered properly.

A quick, honest roadmap of what “deciding what’s next” actually covers

This module deliberately stayed simple — every edge so far has been fixed and unconditional. That’s genuinely not the whole story, and it’s worth being upfront about where the rest of it lives, so you’re not left wondering if this is all there is to routing.

flowchart LR
    A["Normal edges\n(this module)\nfixed, unconditional"] --> B["Conditional edges\n(next module)\nchoose based on state"]
    B --> C["Dynamic routing: Command, Send\n(later modules)\nupdate state AND route,\nor fan out to N branches at runtime"]

Three genuine tiers, each covered fully in its own place. This module built the foundation every one of them sits on top of.

Common mistakes worth avoiding

Forgetting an edge into a node entirely. A node with no incoming edge from START or any other node is simply unreachable — it exists in your code, but the graph will never actually run it, and this fails silently rather than raising an obvious error.

Wiring an edge to the wrong node name as a plain string. builder.add_edge("add_greetng", "add_signature") — a simple typo — genuinely won’t be caught until you try to compile or run the graph, since node names are just strings, not variables your editor can check for you. Double-check node name strings carefully, especially in a genuinely large graph.

Assuming every workflow needs conditional routing from the start. A real, honest number of genuine workflows are fine as a fixed, linear sequence — recall this module’s own three-step example. Don’t reach for the conditional-edge machinery covered next until state actually needs to influence the path.

What you should take away from this module

  • An edge is a fixed, unconditional transition: builder.add_edge("A", "B") means B always runs after A finishes.
  • A simple, fixed sequence of nodes is a real, working instance of the prompt-chaining pattern you already know from your Agent Design Patterns course.
  • Multiple nodes can share the same next node — a real convergence pattern that matters more once parallel execution is covered.
  • Routing genuinely comes in three tiers — normal edges, conditional edges, and dynamic routing via Command/Send — each covered fully in its own place in this course.

Where this goes next

The next module covers START and END properly — why explicit entry and exit points matter, what a genuine accidental infinite workflow looks like, and how a graph can have multiple, entirely different termination routes.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed