Skip to content
← All notes

From state to edges: how LangGraph connects the dots

States, nodes and edges are the whole of LangGraph. What each one is for, how reducers decide what survives an update, and a routing bot built out of all three.

Published
Reading
8 min
Tags
agents
Originally
Codemancers

In LangGraph, everything revolves around three core concepts: states, nodes and edges. Understanding how they work together is the whole of building an effective AI workflow.

States: your data container

States are containers holding all the information a workflow needs. They define what data is available and how it gets updated.

import { Annotation } from '@langchain/langgraph';
import { BaseMessage } from '@langchain/core/messages';
 
// Define what our state looks like
const State = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (existing, newMessages) => existing.concat(newMessages),
    default: () => [],
  }),
  userInput: Annotation<string>({
    reducer: (x, y) => y ?? x,
    default: () => '',
  }),
  step: Annotation<string>({
    reducer: (x, y) => y ?? x,
    default: () => 'start',
  }),
});

The reducer function tells LangGraph how to combine new data with existing data. For messages we append new ones to the list. For simple values like userInput and step we replace the old value with the new one.

Nodes: your processing functions

Nodes are functions that take the current state, do something with it, and return updates to the state.

import { HumanMessage, AIMessage } from '@langchain/core/messages';
import { ChatOpenAI } from '@langchain/openai';
 
const model = new ChatOpenAI({
  modelName: 'gpt-4o',
  temperature: 0.7,
});
 
async function processInput(state: typeof State.State) {
  const { userInput, messages } = state;
 
  // Add the user's message to our conversation
  const userMessage = new HumanMessage(userInput);
  const allMessages = [...messages, userMessage];
 
  // Get a response from the AI
  const response = await model.invoke(allMessages);
 
  // Return updates to the state
  return {
    messages: [response], // Add the AI's response
    step: 'processed', // Update our step
  };
}

Edges: your decision logic

Edges decide which node runs next based on the current state. They're the arrows in a flowchart.

import { END } from '@langchain/langgraph';
 
function decideNext(state: typeof State.State) {
  const { step } = state;
 
  if (step === 'start') {
    return 'processInput';
  } else if (step === 'processed') {
    return 'checkComplete';
  } else if (step === 'complete') {
    return END; // Stop the workflow
  } else {
    return 'processInput'; // Default: keep processing
  }
}

Example: an AI-powered customer support bot

A practical example using the model itself for message categorisation and routing:

import { Annotation, StateGraph, START, END } from '@langchain/langgraph';
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
 
const model = new ChatOpenAI({
  modelName: 'gpt-4o',
  temperature: 0.1, // Low temperature for consistent categorization
});
 
// State for customer support
const SupportState = Annotation.Root({
  userMessage: Annotation<string>({
    reducer: (x, y) => y ?? x,
    default: () => '',
  }),
  category: Annotation<string>({
    reducer: (x, y) => y ?? x,
    default: () => '',
  }),
  response: Annotation<string>({
    reducer: (x, y) => y ?? x,
    default: () => '',
  }),
  step: Annotation<string>({
    reducer: (x, y) => y ?? x,
    default: () => 'start',
  }),
});
 
// AI-powered categorization node
async function categorizeMessage(state: typeof SupportState.State) {
  const { userMessage } = state;
 
  const systemPrompt = `You are a customer support categorization AI.
  Analyze the user's message and categorize it into one of these categories:
  - billing: Payment issues, subscription problems, billing questions
  - technical: App bugs, technical problems, feature issues
  - refunds: Return requests, refund inquiries, cancellation issues
  - general: Greetings, general questions, unclear requests
 
  Respond with ONLY the category name, nothing else.`;
 
  const messages = [
    new SystemMessage(systemPrompt),
    new HumanMessage(userMessage),
  ];
 
  const response = await model.invoke(messages);
  const category = response.content.toString().toLowerCase().trim();
 
  return {
    category,
    step: 'categorized',
  };
}
 
async function handleBilling(state: typeof SupportState.State) {
  const { userMessage } = state;
 
  const systemPrompt = `You are a billing support specialist.
  Help the customer with their billing-related issue. Be helpful and professional.
  If you need to escalate, mention that you'll connect them with a billing specialist.`;
 
  const messages = [
    new SystemMessage(systemPrompt),
    new HumanMessage(userMessage),
  ];
 
  const response = await model.invoke(messages);
 
  return {
    response: response.content.toString(),
    step: 'completed',
  };
}
 
async function handleTechnical(state: typeof SupportState.State) {
  const { userMessage } = state;
 
  const systemPrompt = `You are a technical support specialist.
  Help the customer with their technical issue. Provide troubleshooting steps when possible.
  If the issue is complex, mention that you'll connect them with a technical specialist.`;
 
  const messages = [
    new SystemMessage(systemPrompt),
    new HumanMessage(userMessage),
  ];
 
  const response = await model.invoke(messages);
 
  return {
    response: response.content.toString(),
    step: 'completed',
  };
}
 
async function handleRefunds(state: typeof SupportState.State) {
  const { userMessage } = state;
 
  const systemPrompt = `You are a refunds specialist.
  Help the customer with their refund or return request. Be empathetic and clear about the process.
  Mention that the refunds team will review their request within 24 hours.`;
 
  const messages = [
    new SystemMessage(systemPrompt),
    new HumanMessage(userMessage),
  ];
 
  const response = await model.invoke(messages);
 
  return {
    response: response.content.toString(),
    step: 'completed',
  };
}
 
async function handleGeneral(state: typeof SupportState.State) {
  const { userMessage } = state;
 
  const systemPrompt = `You are a general customer support agent.
  Help the customer with their inquiry. Be friendly and ask clarifying questions if needed.
  If you can't help directly, offer to connect them with the appropriate specialist.`;
 
  const messages = [
    new SystemMessage(systemPrompt),
    new HumanMessage(userMessage),
  ];
 
  const response = await model.invoke(messages);
 
  return {
    response: response.content.toString(),
    step: 'completed',
  };
}
 
function routeByCategory(state: typeof SupportState.State) {
  const { category } = state;
 
  switch (category) {
    case 'billing':
      return 'handleBilling';
    case 'technical':
      return 'handleTechnical';
    case 'refunds':
      return 'handleRefunds';
    default:
      return 'handleGeneral';
  }
}
 
const workflow = new StateGraph(SupportState)
  .addNode('categorizeMessage', categorizeMessage)
  .addNode('handleBilling', handleBilling)
  .addNode('handleTechnical', handleTechnical)
  .addNode('handleRefunds', handleRefunds)
  .addNode('handleGeneral', handleGeneral)
  .addEdge(START, 'categorizeMessage')
  .addConditionalEdges('categorizeMessage', routeByCategory, {
    handleBilling: 'handleBilling',
    handleTechnical: 'handleTechnical',
    handleRefunds: 'handleRefunds',
    handleGeneral: 'handleGeneral',
  })
  .addEdge('handleBilling', END)
  .addEdge('handleTechnical', END)
  .addEdge('handleRefunds', END)
  .addEdge('handleGeneral', END);
 
export const graph = workflow.compile();

The three pillars

States: the foundation. Define what data your workflow needs, use reducers to control how it gets merged, and keep the structure simple and focused.

Nodes: the workers. Each node should have a single responsibility, always return updates to the state, and handle its own errors.

Edges: the decision makers. Use them to create branching logic, base decisions on state values, and keep the functions simple and predictable.

Best practices

  1. Start simple. Begin with basic states, add complexity gradually.
  2. Single responsibility. Each node should do one thing well.
  3. Clear logic. Make edge conditions easy to understand.
  4. Test thoroughly. Try different inputs so the workflow handles every path.

Master these three and you have the foundation for anything more sophisticated.

Resources