Writing your first node function
Why this matters
Every computation in LangGraph happens inside a node function. You need to understand the signature and state flow pattern to build anything beyond a hello-world example.
Explanation
A node function is a Python function that takes the current graph state as input and returns an updated version of that state. It's the atom of computation in LangGraph: every action (calling an LLM, fetching data, making a decision) lives inside a node. Mechanically: when the graph executes, it passes the state dict to your function, your function modifies or enriches it, and returns the new state. LangGraph merges your return value back into the overall state automatically. When to use: anytime you need a discrete step in a workflow: calling an API, processing input, routing decisions, or accumulating results. The node's contract is simple: input state → output state, nothing else.
Analogy
Think of a node like a factory worker at an assembly line. The worker receives a partially-built product (the state), does their job (processes it), and passes it to the next worker with changes they've made. The factory (LangGraph) coordinates who works next based on the edges you define.
Code
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class WorkflowState(TypedDict):
message: str
count: int
def my_first_node(state: WorkflowState) -> WorkflowState:
print(f"Node received: {state}")
return {
"message": f"Processed: {state['message']}",
"count": state["count"] + 1
}
def another_node(state: WorkflowState) -> WorkflowState:
print(f"Second node received: {state}")
return {
"message": state["message"] + " [done]",
"count": state["count"] + 1
}
graph = StateGraph(WorkflowState)
graph.add_node("first", my_first_node)
graph.add_node("second", another_node)
graph.add_edge(START, "first")
graph.add_edge("first", "second")
graph.add_edge("second", END)
compiled_graph = graph.compile()
initial_state = {"message": "hello", "count": 0}
result = compiled_graph.invoke(initial_state)
print(f"\nFinal result: {result}") Node received: {'message': 'hello', 'count': 0}
Second node received: {'message': 'Processed: hello', 'count': 1}
Final result: {'message': 'Processed: hello [done]', 'count': 2} What just happened?
We defined two node functions, each taking a state dict and returning an updated state dict. The graph executed them in sequence: START → first → second → END. The first node incremented count and prefixed the message; the second node appended '[done]' and incremented count again. Each node received the merged output of the previous node.
Common gotcha
Most developers forget that a node function must ALWAYS return a complete state dict (or a dict patch that LangGraph will merge). If you return None or forget to return anything, the state gets lost and the graph crashes or behaves erratically. Also, never mutate the input state directly: always return a new dict or a dict with the keys you're changing.
Error recovery
TypeError: 'NoneType' object is not subscriptableKeyError when accessing state['key']StateGraph requires a TypedDictExperienced dev note
Node functions look simple, but the real insight is: they are pure(ish) transformations. Don't do async I/O, side effects, or expensive operations directly in the node signature: extract those into separate async nodes or use tool_node from langgraph.prebuilt. Your node should be testable in isolation by passing a state dict. This makes debugging workflow logic much faster than trying to trace execution through the whole graph.
Check your understanding
If a node returns only {'message': 'new value'} and the original state had both 'message' and 'count' keys, what happens to the 'count' key in the graph state after this node executes?
Show answer hint
The answer involves understanding that LangGraph has a default merge behavior: it doesn't replace the entire state, it updates/patches it. A correct answer explains whether 'count' is preserved, lost, or requires explicit handling.