Conditional Routing in LangGraph Workflows

Learn how to implement conditional routing in LangGraph. Use Pydantic to structure LLM outputs and route execution dynamically based on sentiment analysis.

Jun 15, 202612 min readFollow

Topics You Will Master

Designing structured output schemas using Pydantic
Integrating LLMs inside custom graph nodes
Building conditional routers based on state parameters
Compiling state graphs with conditional edges

In real-world applications, agentic workflows cannot simply follow a linear sequence of steps. To build responsive systems, your graph needs to make decisions. Conditional Routing is the mechanism in LangGraph that enables dynamic branching, allowing the output of one node to dictate which node is executed next.

This guide demonstrates how to build a social media customer service agent that routes incoming tweets based on their sentiment. The agent uses a local DeepSeek-R1 model to perform sentiment analysis, wraps the model using Pydantic structured schemas, and applies conditional routing logic to generate tone-appropriate responses.

Before proceeding, make sure you are familiar with the core graph components. Refer to the Introduction to LangGraph and Stateful Workflows guide as a prerequisite.

Master LangGraph and LangChain

Agentic RAG and Chatbot, AI Agent with LangChain v1, Qwen3, Gemma3, DeepSeek-R1, LLAMA 3.2, FAISS Vector Database

Enroll on Udemy →

Environment and Model Setup

Ensure you have pulled the deepseek-r1 reasoning model locally using Ollama:

POWERSHELL
ollama pull deepseek-r1

On Linux/macOS:

BASH
ollama pull deepseek-r1

Important

Check this model's license on HuggingFace before any commercial use.

Import the required classes to define State, Pydantic schemas, messages, and graph senders:

PYTHON
from typing_extensions import TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, SystemMessage
from pydantic import BaseModel, Field

# Configuration
BASE_URL = "http://localhost:11434"
MODEL_NAME = "deepseek-r1"

llm = ChatOllama(model=MODEL_NAME, base_url=BASE_URL)

Diagram showing the routing workflow: START to the sentiment analyzer, then a router branching to a positive or negative response node before END

Defining Structured Output Schemas

To ensure our router gets clean data (rather than free-form text), we define a structured Pydantic schema for the sentiment analysis output. This forces the model to return JSON conforming to specific types and descriptions.

Diagram showing Pydantic forcing the model to return typed sentiment, confidence, and reason fields

PYTHON
class SentimentAnalysis(BaseModel):
    sentiment: Literal["positive", "negative"] = Field(
        description="The sentiment classification either positive or negative"
    )
    confidence: float = Field(
        ge=0, le=1.0, description="Confidence score from 0.0 to 1.0"
    )
    reason: str = Field(description="Brief explanation")

Defining the Graph State

Next, declare the schema of the shared graph state. This schema tracks the original tweet input, the analyzed sentiment classification and confidence score, and the final response tweet text.

PYTHON
class SentimentState(TypedDict):
    original_tweet: str
    sentiment: str
    confidence: float
    response_tweet: str

Creating Custom Nodes

Custom nodes execute the step-by-step logic of the workflow. The first node, analyze_sentiment, passes the tweet to the structured LLM wrapper to categorize the feedback:

PYTHON
def analyze_sentiment(state: SentimentState):
    tweet = state['original_tweet']
    print(f"analyzing customer tweet: {tweet}")

    structured_llm = llm.with_structured_output(SentimentAnalysis)

    messages = [
        SystemMessage(
            "Analyze sentiment and provide the structured output. Use 0 to 1.0 scale for confidence. lower is negative and higher is positive"
        ),
        HumanMessage(tweet)
    ]

    analysis = structured_llm.invoke(messages)
    print(f"Sentiment Analysis is done:\n{analysis}")

    return {
        'sentiment': analysis.sentiment,
        'confidence': analysis.confidence
    }

We can test the sentiment analyzer node function in isolation with a sample state:

PYTHON
state = {'original_tweet': "Just launched my new product!"}
analyze_sentiment(state)
PYTHON
analyzing customer tweet: Just launched my new product!
Sentiment Analysis is done:
sentiment='positive' confidence=0.9 reason='The message expresses excitement about launching a new product, indicating a positive sentiment with high confidence due to the enthusiastic tone and lack of negative indicators.'
{'sentiment': 'positive', 'confidence': 0.9}

Now, define the two response-generation nodes. The generated reply adapts its tone according to the confidence score of the sentiment analysis:

PYTHON
def generate_positive_response(state: SentimentState):
    print(f"current state in positive response node: {state}")

    messages = [
        SystemMessage(f"""Generate a warm response to this positive tweet under 280 chars.
                      Confidence: {state['confidence']}. High confidence means be enthusiastic otherwise be friendly."""),
        HumanMessage(state['original_tweet'])
    ]

    response = llm.invoke(messages)
    return {'response_tweet': response.content.strip()}
PYTHON
def generate_negative_response(state: SentimentState):
    print(f"current state in negative response node: {state}")

    messages = [
        SystemMessage(f"""Generate an empathetic response to this negative tweet under 280 chars.
                      If Confidence {state['confidence']} is very low then be empathetic otherwise 
                      be understanding."""),
        HumanMessage(state['original_tweet'])
    ]

    response = llm.invoke(messages)
    return {'response_tweet': response.content.strip()}

Implementing Routing Logic

The routing function does not modify the graph state. Instead, it inspects the state keys (like sentiment) and returns the exact name of the node that the graph runner should transition to.

Diagram showing the router reading the sentiment value and returning the next node's name

PYTHON
def route_by_sentiment(state: SentimentState):
    if state['sentiment'] == 'positive':
        return "positive_response"
    else:
        return "negative_response"

Composing the Router Graph

To define conditional branching in the graph builder, we use builder.add_conditional_edges().

This method takes:

  1. Source Node: The node whose completion triggers the decision (in this case, "analyze").
  2. Path Function: The routing function that returns the next step (route_by_sentiment).
  3. Map: A list or dictionary showing the potential target node names (["positive_response", "negative_response"]).

Diagram showing add_conditional_edges binding a source node, a path function, and a target map

PYTHON
def create_router_graph():
    builder = StateGraph(SentimentState)
    
    # Add nodes
    builder.add_node("analyze", analyze_sentiment)
    builder.add_node("positive_response", generate_positive_response)
    builder.add_node("negative_response", generate_negative_response)

    # Establish entry point
    builder.add_edge(START, "analyze")

    # Establish conditional path decision
    builder.add_conditional_edges(
        "analyze",
        route_by_sentiment,
        ["positive_response", "negative_response"]
    )

    # Establish exit points
    builder.add_edge("positive_response", END)
    builder.add_edge("negative_response", END)

    # Compile the graph
    graph = builder.compile()
    return graph

Compile and inspect the graph structure:

PYTHON
graph = create_router_graph()
graph
OUTPUT
<langgraph.graph.state.CompiledStateGraph object at 0x0000025F26DFFBF0>

Invoking the Workflow

Now, test the routing graph with different user tweets to verify the conditional branch decisions.

Scenario A: Positive Tweet

PYTHON
tweet = "Just launched my new product! the response from everyone has been amazing so far."
result = graph.invoke({'original_tweet': tweet})
result
PYTHON
analyzing customer tweet: Just launched my new product! the response from everyone has been amazing so far.
Sentiment Analysis is done:
sentiment='positive' confidence=0.95 reason="The text expresses excitement and satisfaction with a strong positive adjective ('amazing') and indicates a broad positive reception ('from everyone')."
current state in positive response node: {'original_tweet': 'Just launched my new product! the response from everyone has been amazing so far.', 'sentiment': 'positive', 'confidence': 0.95}
{'original_tweet': 'Just launched my new product! the response from everyone has been amazing so far.',
 'sentiment': 'positive',
 'confidence': 0.95,
 'response_tweet': "OMG that's awesome! 🎉 Congrats on such brilliant feedback! So happy for you and your community! 🔥"}

Scenario B: Negative Tweet

PYTHON
tweet = "Really disappointed with the service I received today."
result = graph.invoke({'original_tweet': tweet})
result
PYTHON
analyzing customer tweet: Really disappointed with the service I received today.
Sentiment Analysis is done:
sentiment='negative' confidence=0.1 reason="The statement expresses clear disappointment with the service, indicating a negative sentiment. The use of 'really' emphasizes the intensity of the negative feeling."
current state in negative response node: {'original_tweet': 'Really disappointed with the service I received today.', 'sentiment': 'negative', 'confidence': 0.1}
{'original_tweet': 'Really disappointed with the service I received today.',
 'sentiment': 'negative',
 'confidence': 0.1,
 'response_tweet': "I'm so sorry to hear that! 😔 I hope things can improve soon."}

Found this useful? Keep building with me.

New tutorials every week on YouTube — or go deeper with a full structured course.

Find this tutorial useful?

Subscribe to our YouTube channels for more practical production walk-throughs.

Discussion & Comments