{"slug": "every-langgraph-pattern-youll-actually-use-explained-properly", "title": "Every LangGraph Pattern You’ll Actually Use : Explained Properly", "summary": "LangGraph offers six workflow patterns for structuring LLM-powered systems, distinguishing between fixed-path workflows and flexible agents. The guide explains that workflows are predictable and cheaper, while agents handle unforeseen steps but risk wandering. LangGraph adds infrastructure like state management and conditional routing that plain Python lacks, making it useful for multi-step chains.", "body_md": "If you’ve looked at LangGraph code before and felt lost in a pile of StateGraph, add_node, and add_edge calls, this guide is for you. We're going to slow down and actually explain what's happening at each step, not just show code and assume it's obvious.\n\nBy the end, you’ll understand six ways to structure an LLM-powered system, when to reach for each one, and how to read (or write) the code without guessing.\n\nThis article is adapted from the reference material: [Workflow And Agents](https://mirror-feeling-d80.notion.site/Workflow-And-Agents-17e808527b1780d792a0d934ce62bee6#180808527b1780639a64c45574fee836)\n\nA single call to an LLM is just a question and an answer. You send a prompt, you get text back, and that’s it. The model has no memory of what it should do next, no way to check its own work, and no ability to call a search engine or a calculator on its own.\n\nThe moment you need more than one step (write a draft, then check it, then fix it), you need something to hold the pieces together: what happened so far, what should happen next, and how to move from one step to the other. That “something” is what this whole guide is about.\n\nThere are two broad ways to build that structure. Anthropic’s own engineering team wrote a good explainer on this, and it’s worth reading directly: [Building Effective Agents](https://www.anthropic.com/research/building-effective-agents).\n\nPeople throw these two words around interchangeably, which causes a lot of confusion. Here’s the distinction that matters:\n\n**A workflow is a fixed path you designed in advance.** You, the developer, decide: step one happens, then step two, maybe with a branch depending on a condition, then step three. The LLM fills in the content at each step, but it doesn’t get to change the route. Think of it like a recipe: the steps are printed on the card, and the cook (the LLM) follows them in order.\n\n**An agent doesn’t have a fixed path.** Instead, it’s given a goal, a set of tools, and the freedom to decide what to do next based on what just happened. It might call a tool, look at the result, decide it needs another tool, call that, and keep going until it thinks the job is done. Nobody wrote down the exact sequence of steps ahead of time. The model works that out as it goes.\n\nNeither one is “better.” Workflows are predictable, easier to test, and cheaper to run, because you know exactly what will happen. Agents are more flexible and can handle problems you didn’t fully anticipate, but that flexibility comes at the cost of predictability. An agent can wander, loop unnecessarily, or make a bad call that a fixed workflow would never have allowed in the first place.\n\nA rough rule of thumb: if you can draw the flowchart before you write any code, build a workflow. If you can’t, because the right next step genuinely depends on things you can’t predict, you probably need an agent.\n\nShort answer: no, not technically. Every pattern in this guide could be written as plain Python: a few functions and some if/else logic. LangGraph doesn’t add new capabilities that Python doesn’t already have.\n\nWhat it adds is infrastructure you’d otherwise have to build yourself, and would probably build badly the first few times:\n\nIf you’re just experimenting with a single prompt, skip LangGraph entirely. Once you’re chaining multiple steps together and need to keep track of what happened, it starts pulling its weight.\n\nEvery code example below follows the same seven-part layout. Learn this once, and you can read any of the patterns further down without re-learning how the code is organized each time.\n\nKeeping this order consistent is the whole point. Once “State” always means the same thing and “Edges” always means the same thing, you stop having to reverse-engineer someone else’s code structure every time you open a new file.\n\nBefore any of the six patterns, you need one core idea: a plain LLM and an LLM that’s been given extra capabilities are not the same thing, and LangChain calls the upgraded version an “augmented” LLM.\n\nOn its own, an LLM just turns text into more text. Two upgrades change what it can do:\n\n**Structured output** forces the model to answer in a specific shape, a Python object with named fields, instead of free-form prose you’d have to parse yourself. You describe the shape you want using a Pydantic model (a class that defines field names, types, and short descriptions), and the LLM fills it in.\n\n**Tool binding** gives the model a menu of functions it’s allowed to call. The LLM doesn’t actually run the function. It decides *whether* a function is needed and *what arguments* to pass, and your code is the one that actually executes it and hands the result back.\n\n``` python\nfrom langchain_anthropic import ChatAnthropic# This is just a plain, unmodified LLM connection.llm = ChatAnthropic(model=\"claude-3-5-sonnet-latest\")\n```\n\nHere’s structured output in practice:\n\n``` python\nfrom pydantic import BaseModel, Field# This class describes the exact shape we want the LLM's answer to take.# Two fields: the search query itself, and a short justification for it.class SearchQuery(BaseModel):    search_query: str = Field(description=\"A web-search-optimized version of the question.\")    justification: str = Field(description=\"Why this query answers the user's request.\")# .with_structured_output() wraps the LLM so it always replies in this shape.structured_llm = llm.with_structured_output(SearchQuery)output = structured_llm.invoke(\"How does a calcium CT score relate to cholesterol?\")print(output.search_query)     # a clean, searchable stringprint(output.justification)    # the model's reasoning for picking that query\n```\n\nWithout with_structured_output, you'd get back a paragraph of text and have to write regex or string-parsing logic to pull the query out of it. That's fragile: the model might phrase things slightly differently every time. Structured output sidesteps the problem entirely by making the shape non-negotiable.\n\nAnd here’s tool binding:\n\n``` php\ndef multiply(a: int, b: int) -> int:    return a * b# bind_tools tells the LLM this function exists and describes what it does,# based on the function's name, type hints, and docstring.llm_with_tools = llm.bind_tools([multiply])msg = llm_with_tools.invoke(\"What is 2 times 3?\")# The LLM doesn't compute 2*3 itself. It recognizes multiply() fits this task# and returns a *request* to call it, with the arguments filled in.msg.tool_calls# -> [{'name': 'multiply', 'args': {'a': 2, 'b': 3}, 'id': '...'}]\n```\n\nNotice the model never actually multiplies anything. It just says “call multiply with a=2, b=3.\" Your code has to be the one that runs it. We'll see exactly how in the full agent example near the end.\n\nWith those two ideas in hand, we can move through the six patterns, from simplest to most autonomous.\n\n**The idea:** break one big task into several smaller LLM calls that run one after another, where each step builds on the output of the one before it. Use this when a task naturally breaks into stages, and getting each stage right on its own is easier than trying to get the whole thing right in a single shot.\n\nA good comparison is editing an essay: you write a rough draft, then you tighten the language, then you do a final polish pass. Each pass has a narrow job, and narrow jobs are easier for an LLM (and for a human) to do well than “write a perfect essay in one attempt.”\n\nOur example: write a joke, improve it with wordplay, then add a twist ending. There’s also a quality check in the middle: if the first draft doesn’t look like a real joke, we stop early instead of wasting two more LLM calls polishing something broken.\n\n```\n# ============================================================# MODULE 1: IMPORTS# ============================================================from typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, END# ============================================================# MODULE 2: STATE# ============================================================# State is just a dictionary with a known shape. Every node function# receives the current state and returns a partial update to it.# LangGraph merges that update back into the shared state automatically.class State(TypedDict):    topic: str    joke: str    improved_joke: str    final_joke: str# ============================================================# MODULE 3: TOOLS# ============================================================# No tools in this pattern. Every step is a plain LLM call.# ============================================================# MODULE 4: NODES# ============================================================def generate_joke(state: State):    \"\"\"Step 1: write a first-draft joke about the topic.\"\"\"    msg = llm.invoke(f\"Write a short joke about {state['topic']}\")    return {\"joke\": msg.content}def improve_joke(state: State):    \"\"\"Step 2: take that draft and add wordplay to it.\"\"\"    msg = llm.invoke(f\"Make this joke funnier by adding wordplay: {state['joke']}\")    return {\"improved_joke\": msg.content}def polish_joke(state: State):    \"\"\"Step 3: give it a final twist ending.\"\"\"    msg = llm.invoke(f\"Add a surprising twist to this joke: {state['improved_joke']}\")    return {\"final_joke\": msg.content}def check_punchline(state: State):    \"\"\"    This isn't a node that generates anything. It's a gate function    used by a conditional edge. It looks at the draft joke and decides    whether it's worth continuing, based on a rough heuristic: does it    contain a '?' or '!'? If not, we treat it as a failed first draft    and stop rather than polishing something broken.    \"\"\"    if \"?\" in state[\"joke\"] or \"!\" in state[\"joke\"]:        return \"Pass\"    return \"Fail\"# ============================================================# MODULE 5: EDGES# ============================================================workflow = StateGraph(State)workflow.add_node(\"generate_joke\", generate_joke)workflow.add_node(\"improve_joke\", improve_joke)workflow.add_node(\"polish_joke\", polish_joke)workflow.add_edge(START, \"generate_joke\")# add_conditional_edges routes to a *different* next node depending on# what the gate function returns. Here: \"Pass\" continues the chain,# \"Fail\" jumps straight to the end.workflow.add_conditional_edges(    \"generate_joke\",    check_punchline,    {\"Pass\": \"improve_joke\", \"Fail\": END},)workflow.add_edge(\"improve_joke\", \"polish_joke\")workflow.add_edge(\"polish_joke\", END)# ============================================================# MODULE 6: ASSEMBLY# ============================================================chain = workflow.compile()# ============================================================# MODULE 7: ENTRYPOINT# ============================================================if __name__ == \"__main__\":    state = chain.invoke({\"topic\": \"cats\"})    print(\"Initial joke:\", state[\"joke\"])    if \"improved_joke\" in state:        print(\"Improved joke:\", state[\"improved_joke\"])        print(\"Final joke:\", state[\"final_joke\"])    else:        print(\"Joke failed the quality gate: no punchline detected.\")\n```\n\nA few things worth slowing down on:\n\n**The idea:** when the sub-tasks don’t depend on each other, run them at the same time instead of one after another. If you’re generating a joke, a story, and a poem about the same topic, there’s no reason the poem has to wait for the joke to finish first. Nothing in the poem depends on the joke.\n\nThis is also useful when you want several independent “opinions” on the same input, for example, asking three different prompts to search for the same information from different angles, then combining what they each found.\n\n```\n# ============================================================# MODULE 1: IMPORTS# ============================================================from typing_extensions import TypedDictfrom langgraph.graph import StateGraph, START, END# ============================================================# MODULE 2: STATE# ============================================================class State(TypedDict):    topic: str    joke: str    story: str    poem: str    combined_output: str# ============================================================# MODULE 3: TOOLS# ============================================================# None needed here either.# ============================================================# MODULE 4: NODES# ============================================================def call_llm_1(state: State):    \"\"\"Generate a joke about the topic.\"\"\"    msg = llm.invoke(f\"Write a joke about {state['topic']}\")    return {\"joke\": msg.content}def call_llm_2(state: State):    \"\"\"Generate a short story about the topic.\"\"\"    msg = llm.invoke(f\"Write a story about {state['topic']}\")    return {\"story\": msg.content}def call_llm_3(state: State):    \"\"\"Generate a poem about the topic.\"\"\"    msg = llm.invoke(f\"Write a poem about {state['topic']}\")    return {\"poem\": msg.content}def aggregator(state: State):    \"\"\"    This node doesn't call the LLM at all. Its only job is to wait    for all three branches to finish, then stitch their outputs    together into one combined string.    \"\"\"    combined = f\"Here's a story, joke, and poem about {state['topic']}!\\n\\n\"    combined += f\"STORY:\\n{state['story']}\\n\\n\"    combined += f\"JOKE:\\n{state['joke']}\\n\\n\"    combined += f\"POEM:\\n{state['poem']}\"    return {\"combined_output\": combined}# ============================================================# MODULE 5: EDGES# ============================================================parallel_builder = StateGraph(State)parallel_builder.add_node(\"call_llm_1\", call_llm_1)parallel_builder.add_node(\"call_llm_2\", call_llm_2)parallel_builder.add_node(\"call_llm_3\", call_llm_3)parallel_builder.add_node(\"aggregator\", aggregator)# All three branches start directly from START. That's what makes# them run in parallel instead of one after another. Then all three# feed into the same aggregator node, which waits until every branch# has completed before it runs.parallel_builder.add_edge(START, \"call_llm_1\")parallel_builder.add_edge(START, \"call_llm_2\")parallel_builder.add_edge(START, \"call_llm_3\")parallel_builder.add_edge(\"call_llm_1\", \"aggregator\")parallel_builder.add_edge(\"call_llm_2\", \"aggregator\")parallel_builder.add_edge(\"call_llm_3\", \"aggregator\")parallel_builder.add_edge(\"aggregator\", END)# ============================================================# MODULE 6: ASSEMBLY# ============================================================parallel_workflow = parallel_builder.compile()# ============================================================# MODULE 7: ENTRYPOINT# ============================================================if __name__ == \"__main__\":    state = parallel_workflow.invoke({\"topic\": \"cats\"})    print(state[\"combined_output\"])\n```\n\nThe key structural difference from prompt chaining: three separate edges leave START, instead of one edge chaining into the next. LangGraph sees that call_llm_1, call_llm_2, and call_llm_3 don't depend on each other's output, so it can run them concurrently and simply wait for all three before moving on to aggregator.\n\n**The idea:** look at the input first, classify what kind of request it is, and send it down a different path depending on the answer. This is useful whenever different types of input genuinely need different handling. A customer support message about billing shouldn’t go through the same logic as a technical bug report.\n\nThe trick is that the “router” itself is just another LLM call, one that uses structured output to guarantee it returns one of a fixed set of labels, rather than a free-text guess you’d have to interpret.\n\n```\n# ============================================================# MODULE 1: IMPORTS# ============================================================from typing_extensions import TypedDict, Literalfrom pydantic import BaseModel, Fieldfrom langchain_core.messages import HumanMessage, SystemMessagefrom langgraph.graph import StateGraph, START, END# ============================================================# MODULE 2: STATE# ============================================================class State(TypedDict):    input: str    decision: str    output: str# ============================================================# MODULE 3: TOOLS# ============================================================# Not a callable tool, but the same idea: a schema that constrains# what the router is allowed to say. Literal[...] means the model# can ONLY answer with one of these three exact strings.class Route(BaseModel):    step: Literal[\"poem\", \"story\", \"joke\"] = Field(        description=\"The next step in the routing process.\"    )router = llm.with_structured_output(Route)# ============================================================# MODULE 4: NODES# ============================================================def llm_call_1(state: State):    \"\"\"Handle requests classified as 'story'.\"\"\"    result = llm.invoke(state[\"input\"])    return {\"output\": result.content}def llm_call_2(state: State):    \"\"\"Handle requests classified as 'joke'.\"\"\"    result = llm.invoke(state[\"input\"])    return {\"output\": result.content}def llm_call_3(state: State):    \"\"\"Handle requests classified as 'poem'.\"\"\"    result = llm.invoke(state[\"input\"])    return {\"output\": result.content}def llm_call_router(state: State):    \"\"\"    Classify the user's input into one of three buckets, using the    Route schema above so the answer is guaranteed to be exactly    \"story\", \"joke\", or \"poem\", never something unexpected like    \"Story!\" or \"I think it's a joke.\"    \"\"\"    decision = router.invoke([        SystemMessage(content=\"Route the input to story, joke, or poem based on the user's request.\"),        HumanMessage(content=state[\"input\"]),    ])    return {\"decision\": decision.step}def route_decision(state: State):    \"\"\"    Gate function for the conditional edge. Reads the classification    made above and returns the name of the node that should handle it.    \"\"\"    if state[\"decision\"] == \"story\":        return \"llm_call_1\"    elif state[\"decision\"] == \"joke\":        return \"llm_call_2\"    elif state[\"decision\"] == \"poem\":        return \"llm_call_3\"# ============================================================# MODULE 5: EDGES# ============================================================router_builder = StateGraph(State)router_builder.add_node(\"llm_call_1\", llm_call_1)router_builder.add_node(\"llm_call_2\", llm_call_2)router_builder.add_node(\"llm_call_3\", llm_call_3)router_builder.add_node(\"llm_call_router\", llm_call_router)router_builder.add_edge(START, \"llm_call_router\")router_builder.add_conditional_edges(    \"llm_call_router\",    route_decision,    {        \"llm_call_1\": \"llm_call_1\",        \"llm_call_2\": \"llm_call_2\",        \"llm_call_3\": \"llm_call_3\",    },)router_builder.add_edge(\"llm_call_1\", END)router_builder.add_edge(\"llm_call_2\", END)router_builder.add_edge(\"llm_call_3\", END)# ============================================================# MODULE 6: ASSEMBLY# ============================================================router_workflow = router_builder.compile()# ============================================================# MODULE 7: ENTRYPOINT# ============================================================if __name__ == \"__main__\":    state = router_workflow.invoke({\"input\": \"Write me a joke about cats\"})    print(state[\"output\"])\n```\n\nNotice llm_call_1, llm_call_2, and llm_call_3 are nearly identical here. In a real project, they'd usually differ by using different system prompts, different tools, or even entirely different models suited to each type of task. The point of routing isn't the destination nodes; it's that the router reliably sends each input to the right specialist.\n\n**The idea:** one LLM call plans out an unknown number of sub-tasks, then a worker handles each one (in parallel) and a final step stitches all the results together. The key difference from plain parallelization is that you don’t know ahead of time how many parallel branches you’ll need. In Pattern 2, we hardcoded exactly three branches (joke, story, poem). Here, the number of branches is decided at runtime by the orchestrator itself.\n\nA good real-world case: writing a report. You don’t know in advance how many sections the report needs. That depends on the topic. So you let an LLM plan the sections first, and only then do you know how many workers to spin up.\n\nThis needs two new pieces of machinery:\n\n```\n# ============================================================# MODULE 1: IMPORTS# ============================================================import operatorfrom typing import Annotated, Listfrom typing_extensions import TypedDictfrom pydantic import BaseModel, Fieldfrom langchain_core.messages import HumanMessage, SystemMessagefrom langgraph.graph import StateGraph, START, ENDfrom langgraph.constants import Send# ============================================================# MODULE 2: STATE# ============================================================class Section(BaseModel):    name: str = Field(description=\"Name for this section of the report.\")    description: str = Field(description=\"What this section should cover.\")class Sections(BaseModel):    sections: List[Section] = Field(description=\"All sections of the report.\")# The main graph state.class State(TypedDict):    topic: str    sections: list[Section]    # Annotated[list, operator.add] is the important part here: it tells    # LangGraph that when multiple workers write to \"completed_sections\"    # at the same time, it should APPEND their results together rather    # than one worker's write erasing another's.    completed_sections: Annotated[list, operator.add]    final_report: str# A separate, smaller state just for what an individual worker needs.# It only has to know about the one section it's writing.class WorkerState(TypedDict):    section: Section    completed_sections: Annotated[list, operator.add]# ============================================================# MODULE 3: TOOLS# ============================================================planner = llm.with_structured_output(Sections)# ============================================================# MODULE 4: NODES# ============================================================def orchestrator(state: State):    \"\"\"Ask the LLM to plan out how many sections the report needs, and what each covers.\"\"\"    report_sections = planner.invoke([        SystemMessage(content=\"Generate a plan for the report.\"),        HumanMessage(content=f\"Here is the report topic: {state['topic']}\"),    ])    return {\"sections\": report_sections.sections}def llm_call(state: WorkerState):    \"\"\"A single worker: writes the content for exactly one section.\"\"\"    section = llm.invoke([        SystemMessage(content=\"Write a report section.\"),        HumanMessage(content=f\"Section name: {state['section'].name}\\nDescription: {state['section'].description}\"),    ])    # Wrapped in a list because operator.add expects to concatenate lists.    return {\"completed_sections\": [section.content]}def synthesizer(state: State):    \"\"\"Once every worker has finished, join all the sections into one document.\"\"\"    completed_report_sections = \"\\n\\n---\\n\\n\".join(state[\"completed_sections\"])    return {\"final_report\": completed_report_sections}def assign_workers(state: State):    \"\"\"    This is what makes the dynamic fan-out possible. Send(node_name, payload)    schedules one run of \"llm_call\" per section the orchestrator planned.    If the plan has 3 sections, this creates 3 parallel workers; if it has    7, it creates 7. We never had to know that number in advance.    \"\"\"    return [Send(\"llm_call\", {\"section\": s}) for s in state[\"sections\"]]# ============================================================# MODULE 5: EDGES# ============================================================orchestrator_worker_builder = StateGraph(State)orchestrator_worker_builder.add_node(\"orchestrator\", orchestrator)orchestrator_worker_builder.add_node(\"llm_call\", llm_call)orchestrator_worker_builder.add_node(\"synthesizer\", synthesizer)orchestrator_worker_builder.add_edge(START, \"orchestrator\")# Instead of add_conditional_edges (which picks ONE next node),# we use add_conditional_edges with assign_workers, which can spawn# MANY next nodes at once via Send().orchestrator_worker_builder.add_conditional_edges(    \"orchestrator\", assign_workers, [\"llm_call\"])orchestrator_worker_builder.add_edge(\"llm_call\", \"synthesizer\")orchestrator_worker_builder.add_edge(\"synthesizer\", END)# ============================================================# MODULE 6: ASSEMBLY# ============================================================orchestrator_worker = orchestrator_worker_builder.compile()# ============================================================# MODULE 7: ENTRYPOINT# ============================================================if __name__ == \"__main__\":    state = orchestrator_worker.invoke({\"topic\": \"Create a report on LLM scaling laws\"})    print(state[\"final_report\"])\n```\n\nIf you only remember one thing from this pattern, make it this: Send is for when the *number* of parallel branches isn't known until the graph is already running. Everything else about it (nodes, edges, state) works the same way as the patterns before it.\n\n**The idea:** one LLM generates something, a second LLM call grades it, and if it doesn’t pass, the feedback gets fed back into another generation attempt, looping until it’s good enough. This is essentially a built-in editor that won’t let a weak first draft through.\n\nThis is worth reaching for whenever “good” is something you can actually check for: grading a joke as funny or not, checking a generated SQL query for syntax errors, or verifying a RAG answer is actually backed by the retrieved documents rather than invented.\n\n```\n# ============================================================# MODULE 1: IMPORTS# ============================================================from typing_extensions import TypedDict, Literalfrom pydantic import BaseModel, Fieldfrom langgraph.graph import StateGraph, START, END# ============================================================# MODULE 2: STATE# ============================================================class State(TypedDict):    joke: str    topic: str    feedback: str    funny_or_not: str# ============================================================# MODULE 3: TOOLS# ============================================================class Feedback(BaseModel):    grade: Literal[\"funny\", \"not funny\"] = Field(description=\"Whether the joke is funny.\")    feedback: str = Field(description=\"If not funny, concrete advice on how to fix it.\")evaluator = llm.with_structured_output(Feedback)# ============================================================# MODULE 4: NODES# ============================================================def llm_call_generator(state: State):    \"\"\"    Write a joke. If this is a retry (there's feedback in state from a    previous failed attempt), pass that feedback back to the model so    it actually improves rather than generating a fresh random attempt.    \"\"\"    if state.get(\"feedback\"):        msg = llm.invoke(            f\"Write a joke about {state['topic']} but take into account this feedback: {state['feedback']}\"        )    else:        msg = llm.invoke(f\"Write a joke about {state['topic']}\")    return {\"joke\": msg.content}def llm_call_evaluator(state: State):    \"\"\"Grade the joke and, if it fails, explain why.\"\"\"    grade = evaluator.invoke(f\"Grade the joke: {state['joke']}\")    return {\"funny_or_not\": grade.grade, \"feedback\": grade.feedback}def route_joke(state: State):    \"\"\"Gate function: accept and stop, or loop back with feedback.\"\"\"    if state[\"funny_or_not\"] == \"funny\":        return \"Accepted\"    return \"Rejected + Feedback\"# ============================================================# MODULE 5: EDGES# ============================================================optimizer_builder = StateGraph(State)optimizer_builder.add_node(\"llm_call_generator\", llm_call_generator)optimizer_builder.add_node(\"llm_call_evaluator\", llm_call_evaluator)optimizer_builder.add_edge(START, \"llm_call_generator\")optimizer_builder.add_edge(\"llm_call_generator\", \"llm_call_evaluator\")# This is the loop: if the evaluator says \"Rejected + Feedback\", control# goes BACK to llm_call_generator instead of forward to END. That's what# turns this into an iterative refinement loop rather than a straight line.optimizer_builder.add_conditional_edges(    \"llm_call_evaluator\",    route_joke,    {\"Accepted\": END, \"Rejected + Feedback\": \"llm_call_generator\"},)# ============================================================# MODULE 6: ASSEMBLY# ============================================================optimizer_workflow = optimizer_builder.compile()# ============================================================# MODULE 7: ENTRYPOINT# ============================================================if __name__ == \"__main__\":    state = optimizer_workflow.invoke({\"topic\": \"Cats\"})    print(state[\"joke\"])\n```\n\nOne practical warning: nothing in this graph stops it from looping forever if the evaluator is a harsh grader and the generator can’t satisfy it. In real projects, you’d usually add a retry counter to State and force an exit after, say, three attempts. Otherwise, a stubborn evaluator can burn through your API budget without you noticing.\n\n**The idea:** this is where we stop pre-drawing the flowchart. Instead of you deciding the sequence of steps, the LLM decides (on every turn) whether it needs to call a tool, and it keeps looping between “think” and “act” until it decides the job is done.\n\nThis is genuinely different from every pattern above. In prompt chaining, routing, and the rest, *you* wrote down which node comes after which. Here, there’s really only one loop: the model thinks, optionally calls a tool, sees the result, and thinks again, and it can go around that loop as many times as it needs to.\n\nUse this when the task is open-ended enough that you can’t lay out the steps in advance, for instance, a multi-step calculation where you don’t know how many operations will be needed until you’re partway through it.\n\n```\n# ============================================================# MODULE 1: IMPORTS# ============================================================from typing_extensions import Literalfrom langchain_core.tools import toolfrom langchain_core.messages import SystemMessage, ToolMessage, HumanMessagefrom langgraph.graph import StateGraph, START, END, MessagesState# ============================================================# MODULE 2: STATE# ============================================================# MessagesState is a ready-made state type from LangGraph that already# has one field, \"messages,\" designed to hold a running list of chat# messages. We don't need to define our own State class this time# because a conversation history is exactly what this pattern needs.# ============================================================# MODULE 3: TOOLS# ============================================================# The @tool decorator turns a normal Python function into something# the LLM can request to call. The docstring matters. The model reads# it to decide when this tool is the right one to use.@tooldef multiply(a: int, b: int) -> int:    \"\"\"Multiply a and b.\"\"\"    return a * b@tooldef add(a: int, b: int) -> int:    \"\"\"Add a and b.\"\"\"    return a + b@tooldef divide(a: int, b: int) -> float:    \"\"\"Divide a and b.\"\"\"    return a / btools = [add, multiply, divide]tools_by_name = {t.name: t for t in tools}llm_with_tools = llm.bind_tools(tools)# ============================================================# MODULE 4: NODES# ============================================================def llm_call(state: MessagesState):    \"\"\"    The \"thinking\" step. The model looks at the full conversation so far    and decides: answer directly, or request a tool call. Either way,    its response gets appended to the messages list.    \"\"\"    return {        \"messages\": [            llm_with_tools.invoke(                [SystemMessage(content=\"You are a helpful assistant tasked with performing arithmetic on a set of inputs.\")]                + state[\"messages\"]            )        ]    }def tool_node(state: dict):    \"\"\"    The \"acting\" step. This is where a tool call the model requested    actually gets executed. The LLM never runs code itself, it only    asks for it. We look up the requested tool by name, run it with    the arguments the model provided, and package the result as a    ToolMessage so the model can read it on its next turn.    \"\"\"    result = []    for tool_call in state[\"messages\"][-1].tool_calls:        selected_tool = tools_by_name[tool_call[\"name\"]]        observation = selected_tool.invoke(tool_call[\"args\"])        result.append(ToolMessage(content=observation, tool_call_id=tool_call[\"id\"]))    return {\"messages\": result}def should_continue(state: MessagesState) -> Literal[\"environment\", END]:    \"\"\"    This is the heart of the agent loop. After the model thinks, we check:    did it ask for a tool? If yes, go run the tool (\"Action\"). If not,    meaning it gave a final answer instead, the loop ends.    \"\"\"    last_message = state[\"messages\"][-1]    if last_message.tool_calls:        return \"Action\"    return END# ============================================================# MODULE 5: EDGES# ============================================================agent_builder = StateGraph(MessagesState)agent_builder.add_node(\"llm_call\", llm_call)agent_builder.add_node(\"environment\", tool_node)agent_builder.add_edge(START, \"llm_call\")agent_builder.add_conditional_edges(    \"llm_call\",    should_continue,    {\"Action\": \"environment\", END: END},)# This is the edge that makes it a loop rather than a straight line:# after a tool runs, control goes back to llm_call, not forward to END.# The model gets to see the tool's result and decide what to do next:# answer, or call another tool.agent_builder.add_edge(\"environment\", \"llm_call\")# ============================================================# MODULE 6: ASSEMBLY# ============================================================agent = agent_builder.compile()# ============================================================# MODULE 7: ENTRYPOINT# ============================================================if __name__ == \"__main__\":    messages = [HumanMessage(content=\"Add 3 and 4. Then take the output and multiply it by 4.\")]    result = agent.invoke({\"messages\": messages})    for m in result[\"messages\"]:        m.pretty_print()\n```\n\nWalk through what actually happens on that input, step by step:\n\nNobody wrote “call add, then call multiply” anywhere in the code. The model figured that sequence out on its own, one decision at a time. That’s the entire difference between an agent and a workflow.\n\nStart simple and add complexity only when you hit its limits:\n\n[Every LangGraph Pattern You’ll Actually Use : Explained Properly](https://pub.towardsai.net/every-langgraph-pattern-youll-actually-use-explained-properly-a3fc2f947075) was originally published in [Towards AI](https://pub.towardsai.net) on Medium, where people are continuing the conversation by highlighting and responding to this story.", "url": "https://wpnews.pro/news/every-langgraph-pattern-youll-actually-use-explained-properly", "canonical_source": "https://pub.towardsai.net/every-langgraph-pattern-youll-actually-use-explained-properly-a3fc2f947075?source=rss----98111c9905da---4", "published_at": "2026-07-22 18:01:01+00:00", "updated_at": "2026-07-22 18:29:06.344128+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-agents"], "entities": ["LangGraph", "Anthropic", "LangChain"], "alternates": {"html": "https://wpnews.pro/news/every-langgraph-pattern-youll-actually-use-explained-properly", "markdown": "https://wpnews.pro/news/every-langgraph-pattern-youll-actually-use-explained-properly.md", "text": "https://wpnews.pro/news/every-langgraph-pattern-youll-actually-use-explained-properly.txt", "jsonld": "https://wpnews.pro/news/every-langgraph-pattern-youll-actually-use-explained-properly.jsonld"}}