β¬
οΈ Previous - Intro to LangGraph
β‘οΈ Next - LangGraph Writer-Critic Loop
In this lesson, youβll build your first LangGraph project β a simple joke bot β to learn how to define state, create nodes, add conditional edges, and run a stateful workflow. No LLMs yet β just clean, inspectable graph logic to help you master the fundamentals.
Before we dive into tools, agents, and LLMs, letβs master the fundamentals of LangGraph β how to build and run a graph-based workflow.
To keep things simple (and fun), weβll build a joke bot that tells you jokes from different categories. Thereβs no LLM here β and thatβs intentional. This example is not about building a useful chatbot. Itβs about helping you:
LangGraph is typically used with agents and language models. But here, weβre skipping all that to focus on the core mechanics β so when we add complexity later, youβll know exactly whatβs happening under the hood.
Youβll learn how to:
By the end, youβll have a running LangGraph app β and a clear mental model for how LangGraph systems work.
Before you dive into the project, take a quick look at the joke bot youβre about to build. This video demo walks through the full user experience β from choosing jokes to switching categories β so you can see how LangGraph drives the logic behind the scenes.
Itβs short, fun, and gives you a clear picture of the project outcome.
Your project is a simple and playful joke-telling bot, built with LangGraph. We will be using the pyjokes
library to fetch jokes - there are no LLM calls involved in this example.
Hereβs how it works:
The bot keeps track of two key things:
Youβll build reusable functions as graph nodes, route dynamically based on your choices, and see how LangGraph evolves state across steps. By the end, youβll understand how a graph-based workflow actually runs β not just in theory, but in code.
And yes, it might make you laugh too π
Letβs get started.
Before we write any code, letβs take a look at the structure of the graph youβll be building.
Hereβs the Mermaid diagram that shows how the nodes connect:
This flow might look simple β and thatβs the point. Itβs meant to teach you the LangGraph mechanics with just enough logic to be interesting.
Letβs walk through the key parts:
πͺ show_menu
This is the main control node. Every time it runs, it shows the user three options:
Based on the userβs input, the graph routes to one of the next nodes.
π© fetch_joke
This node fetches a joke from the selected category and displays it. After showing the joke, it routes back to show_menu
so the user can make another choice.
π¦ update_category
This lets the user select a new category of jokes (like βchuckβ or βneutralβ). After the update, it also routes back to the menu.
β exit_bot
When the user chooses to quit, this node says goodbye and ends the graph by routing to the special __end__
node.
The entire system loops until the user explicitly chooses to quit.
This setup gives us a great little sandbox to practice:
Now that youβve seen the structure, letβs start building it step by step.
In the last lesson, you learned the five steps to building any LangGraph system:
Weβll follow the same structure here β but this time, weβre doing it hands-on.
We start by declaring what the bot should remember β the current joke category, what the user just selected, and a list of jokes already told. We also add a quit
flag for graceful exit.
class Joke(BaseModel): text: str category: str class JokeState(BaseModel): jokes: Annotated[List[Joke], add] = [] jokes_choice: Literal["n", "c", "q"] = "n" # next joke, change category, or quit category: str = "neutral" language: str = "en" quit: bool = False
We use Annotated
with add
to automatically append new jokes to the list.
Note that the reducer for jokes
will append new jokes to the existing list, allowing us to keep track of whatβs been told so far. For the other fields, we are using default reducer which simply overwrites the current value. This will be handled by LangGraph automatically.
Each node is just a function that takes in the state and returns updates. We have four simple nodes:
Here's the show_menu
function that displays the menu and gets user input:
def show_menu(state: JokeState) -> dict: user_input = input("[n] Next [c] Category [q] Quit\n> ").strip().lower() return {"jokes_choice": user_input}
Now, let's implement fetch_joke
, which pulls a joke from the pyjokes
library and returns it:
def fetch_joke(state: JokeState) -> dict: joke_text = get_joke(language=state.language, category=state.category) new_joke = Joke(text=joke_text, category=state.category) return {"jokes": [new_joke]}
Next, we implement the update_category
function, which allows the user to change the joke category:
def update_category(state: JokeState) -> dict: categories = ["neutral", "chuck", "all"] selection = int(input("Select category [0=neutral, 1=chuck, 2=all]: ").strip()) return {"category": categories[selection]}
For our last node, we implement the exit_bot
function, which will be called when the user chooses to quit:
def exit_bot(state: JokeState) -> dict: return {"quit": True}
We also define a router function to decide which node to go to next. This will serve as a conditional edge based on the user's choice:
def route_choice(state: JokeState) -> str: if state.jokes_choice == "n": return "fetch_joke" elif state.jokes_choice == "c": return "update_category" elif state.jokes_choice == "q": return "exit_bot" return "exit_bot"
You might have noticed that none of our node functions mutate the state directly. Instead, each node simply returns a dictionary of field updates.
This is by design.
LangGraph handles state management for you β it takes the updates you return and produces a new state object behind the scenes. This means:
{field_name: new_value}
.This declarative approach keeps your logic clean, predictable, and easy to debug β especially as your graphs grow in complexity.
So: just return what changed, and let LangGraph handle the rest.
We will use a builder function to create our graph. This is where we define the structure of our workflow and register all the nodes we created earlier.
def build_joke_graph() -> CompiledStateGraph: workflow = StateGraph(JokeState) workflow.add_node("show_menu", show_menu) workflow.add_node("fetch_joke", fetch_joke) workflow.add_node("update_category", update_category) workflow.add_node("exit_bot", exit_bot) workflow.set_entry_point("show_menu") workflow.add_conditional_edges( "show_menu", route_choice, { "fetch_joke": "fetch_joke", "update_category": "update_category", "exit_bot": "exit_bot", } ) workflow.add_edge("fetch_joke", "show_menu") workflow.add_edge("update_category", "show_menu") workflow.add_edge("exit_bot", END) return workflow.compile()
This function instantiates the StateGraph
, registers our nodes, sets the entry point, and defines the routing logic. We are also compiling the graph at this point, which prepares it for execution.
Once everything is wired up, we can have the main function to run the graph. This is where we invoke the graph with an initial state and start the interaction loop.
def main(): graph = build_joke_graph() final_state = graph.invoke(JokeState(), config={"recursion_limit": 100})
Thatβs it β your first full LangGraph in action.
In the next section, weβll run it and show what it looks like in action.
Now that everythingβs wired up, letβs see your LangGraph joke bot in action.
Weβve kept the lesson code clean and minimal, but the actual project includes a few extra print statements for a better user experience β youβll find those in the repo if you want to add some flair.
When you run the script, youβll get a simple but functional interactive menu. You can:
β οΈ Yes, the UI is extremely... minimalist. Weβre here to learn LangGraph, not win design awards π
Hereβs what a sample session might look like:
π==========================================================π WELCOME TO THE LANGGRAPH JOKE BOT! This example demonstrates agentic state flow without LLMs ============================================================ π==========================================================π STARTING JOKE BOT SESSION... ============================================================ π Menu | Category: NEUTRAL | Jokes: 0 -------------------------------------------------- Pick an option: [n] π Next Joke [c] π Change Category [q] πͺ Quit User Input: n π 3 Database Admins walked into a NoSQL bar. A little while later they walked out because they couldn't find a table. ============================================================ π Menu | Category: NEUTRAL | Jokes: 1 -------------------------------------------------- Pick an option: [n] π Next Joke [c] π Change Category [q] πͺ Quit User Input: n π Hardware: The part of a computer that you can kick. ============================================================ π Menu | Category: NEUTRAL | Jokes: 2 -------------------------------------------------- Pick an option: [n] π Next Joke [c] π Change Category [q] πͺ Quit User Input: q πͺ==========================================================πͺ GOODBYE! ============================================================ π==========================================================π SESSION COMPLETE! ============================================================ π You enjoyed 2 jokes during this session! π Final category: NEUTRAL π Thanks for using the LangGraph Joke Bot! ============================================================
You can see the full loop in action: input β state update β reroute β repeat.
Notice how:
Itβs simple, predictable, and easy to extend β which is exactly what you want in the early stages of building agentic workflows.
Letβs take it further.
Now that you've built your first LangGraph system, it's time to flex those new muscles. Try extending the joke bot with a few extra features. These will push you to think about state design, routing, and node logic β just like youβll need to do in real agentic systems.
Here are a couple of fun upgrades β and a bonus challenge for those ready to bring in an LLM:
pyjokes
supports multiple languages (like en
, de
, es
, etc.). Letβs make our bot multilingual!
What to do:
JokeState
called language
update_language
) that lets the user pick a language[l] π Change Language
) and update the routing logicLetβs give the user a way to reset the joke history.
What to do:
[r] π Reset Joke History
reset_jokes
) that clears the list of past jokespyjokes
In the next lesson, weβll replace pyjokes
with an actual LLM call β but if you want to try it yourself now, hereβs the challenge:
What to try:
Swap the fetch_joke
node logic with an LLM call (OpenAI, Anthropic, Mistral, etc.)
Design a prompt that returns a single joke for the selected category
Use category
to control the theme:
"programmer"
β general dev jokes"chuck"
β Chuck Norris jokes"dad"
β classic dad jokes (or whatever your LLM supports)Weβll walk through one possible implementation in the next lesson β but if you're ready to explore early, go for it.
You just built and ran your first LangGraph project β no LLMs, no agents, just clean graph logic.
Along the way, you:
And thanks to LangGraph, you didnβt have to worry about:
Itβs not the prettiest UI β but that wasnβt the point.
You now know how LangGraph workflows really run β and how to build them from the ground up.
Next up: LangSmith β a tool that helps you debug, trace, and improve your graph-based systems.
Onward.
β¬
οΈ Previous - Intro to LangGraph
β‘οΈ Next - LangGraph Writer-Critic Loop