AI Workflows

Part 2 — Turn Two Python Functions into Your First LangGraph

CodeTips Academy
October 24, 2026
12 min read
54 views
Turn Two Python Functions into Your First LangGraph

Build and run a StateGraph, then trace how nodes return changes to a shared dictionary-shaped state.

Complete code for Part 2: Download this standalone lesson · Download the full series project. This article shows every required code file and import. Extract the lesson folder and follow its requirements.txt and README.

What you will build

The previous lesson placed function calls in a fixed order. That is enough for two simple steps. We now describe that order explicitly so later lessons can add branches and pauses without hiding the control flow inside one large function.

Our example still does only two things: clean the question and prepare a reply. Introducing LangGraph does not make the reply smarter. It gives us a runnable map of which Python function executes next. We will distinguish that map from the data dictionary it processes.

By the end: Build and execute a two-node graph, then explain every field in its final state.

What you need before starting

You need basic Python: variables, functions, dictionaries, loops, and imports. When this lesson introduces a class or a library-specific term, its role is explained below.

Use Python 3.12. These examples were executed on Windows with Python 3.12.4. Use the shown interpreter consistently when installing packages and running code.

You do not need to run an earlier article to make this part execute. The complete files and data used here are supplied below. The series order is for learning the ideas progressively.

Understand the idea before the code

This lesson still does the same small job: clean a question, then prepare a reply. A workflow is those operations in a chosen order. LangGraph lets us describe that order as a map and run it.

State means the current data for one run. In this lesson it is a normal Python dictionary. Before any step runs, it might contain only {"text": " Python help "}. After cleaning, the dictionary also contains cleaned. After drafting, it also contains reply. State is the data on the workbench; it is not the program that decides what happens next.

A node is a named step, implemented here by a Python function. An edge says which step may run next. A graph is the collection of those nodes and edges: our workflow map. The arrows describe execution order. The functions read values from state, rather than receiving a special value carried inside an arrow.

Name in the example What it is What it does not contain
State A TypedDict class describing allowed field names and expected value types. This is the state schema. The actual question for a particular run.
state A function parameter referring to the current data dictionary. The workflow's nodes and edges.
StateGraph The library class used to create a workflow builder. A trained model or the result of running the workflow.
builder One builder object created by StateGraph(State). You register steps and connections on it. The final reply.
graph A variable name we use for the runnable object returned by builder.compile(). A plotted chart. The variable could have another name.

The workflow we are building

Graph generated by this lesson's compiled program

The boxes are named Python operations. Solid arrows are fixed transitions; dashed arrows show possible destinations chosen by routing functions. The image comes from this lesson's actual compiled graph. One particular input follows only the applicable route. Open the full-size graph.

Create the lesson folder

Create this folder and keep the filenames exactly as shown. In particular, a local module is an ordinary file beside the program, not an extra package you must guess how to install.

lesson-folder/
  lesson.py
  graph_picture.py
  requirements.txt

Save requirements.txt

These are the external Python packages for this lesson. The local files above are included in the article; they are not pip packages.

langgraph==1.2.16
langgraph-checkpoint-sqlite==3.1.2
python -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt

On Linux/macOS, use python3 -m venv .venv and .venv/bin/python -m pip install -r requirements.txt. Installation requires internet access. The default examples do not call a paid API.

Complete code for this article

Save this complete file as lesson.py

This is the complete program, including imports, data, function definitions, and the entry point.

from graph_picture import save_graph

"""Article 2: put the two Python steps into a StateGraph."""
import json
from typing import TypedDict
from langgraph.graph import END, START, StateGraph


class State(TypedDict, total=False):
    text: str
    cleaned: str
    reply: str


def clean(state):
    return {"cleaned": " ".join(state["text"].split())}


def draft(state):
    return {"reply": "Check the environment for this question: " + state["cleaned"]}


def build():
    builder = StateGraph(State)
    builder.add_node("clean", clean)
    builder.add_node("draft", draft)
    builder.add_edge(START, "clean")
    builder.add_edge("clean", "draft")
    builder.add_edge("draft", END)
    compiled = builder.compile()
    save_graph(compiled, 2)
    return compiled


if __name__ == "__main__":
    print(
        json.dumps(
            build().invoke({"text": "  My Python module   is missing.  "}), indent=2
        )
    )

Save this complete file as graph_picture.py

This is the complete local diagram renderer imported by lesson.py. Save it in the same folder. It draws the compiled graph; it does not decide the workflow or contact an image service.

import html
from pathlib import Path

ROLES = {
    "__start__": "Enter the workflow",
    "__end__": "This run is finished",
    "clean": "Normalize the question",
    "draft": "Prepare a reply",
    "classify": "Choose a topic category",
    "technical": "Technical branch",
    "human": "Ask a mentor to review",
    "lookup": "Retrieve local evidence",
    "check": "Check source-ID structure",
    "revise": "Allow one more draft",
    "review": "Pause inside this node",
    "approval": "Wait for a human decision",
    "queue": "Write one local outbox row",
    "escalate": "Finish with needs_human",
}


def positions(part):
    if part == 2:
        return {
            "__start__": (320, 115),
            "clean": (320, 250),
            "draft": (320, 385),
            "__end__": (320, 520),
        }
    if part == 3:
        return {
            "__start__": (420, 115),
            "classify": (420, 250),
            "human": (120, 410),
            "technical": (720, 410),
            "__end__": (420, 590),
        }
    if part == 5:
        return {"__start__": (320, 115), "review": (320, 265), "__end__": (320, 415)}
    return {
        "__start__": (420, 115),
        "classify": (420, 250),
        "lookup": (420, 385),
        "draft": (420, 520),
        "check": (420, 655),
        "revise": (790, 655),
        "human": (70, 850),
        "escalate": (70, 850),
        "approval": (420, 850),
        "queue": (420, 1035),
        "__end__": (420, 1215),
    }


def draw_svg(structure, part):
    layout = positions(part)
    node_width, node_height = (225, 83)
    width = 850 if part in {2, 5} else 1080
    height = max((layout[name][1] for name in structure.nodes)) + node_height + 100
    chunks = [
        f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" role="img" aria-labelledby="title desc">',
        f'<title id="title">Part {part}: graph generated from the compiled lesson code</title>',
        '<desc id="desc">Solid arrows are fixed edges. Dashed arrows are conditional destinations. Node positions are chosen for readability.</desc>',
        '<defs><marker id="solid" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0 0 L10 5 L0 10z" fill="#526c75"/></marker><marker id="conditional" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0 0 L10 5 L0 10z" fill="#b54625"/></marker></defs>',
        '<rect width="100%" height="100%" fill="#faf9f5"/>',
        '<text x="30" y="35" font-family="Arial,sans-serif" font-size="16" font-weight="bold" fill="#b54625">CODETIPS ACADEMY / GENERATED FROM CODE</text>',
        f'<text x="30" y="73" font-family="Arial,sans-serif" font-size="28" font-weight="bold" fill="#183443">Part {part} — the compiled workflow</text>',
    ]
    edge_data = []
    for index, edge in enumerate(structure.edges):
        sx, sy = layout[edge.source]
        tx, ty = layout[edge.target]
        if sx == tx:
            points = [
                (sx + node_width / 2, sy + node_height),
                (tx + node_width / 2, ty),
            ]
        elif ty < sy:
            points = [
                (sx + node_width / 2, sy),
                (sx + node_width / 2, ty + node_height / 2),
                (tx + node_width, ty + node_height / 2),
            ]
        else:
            leftward = tx < sx
            x1 = sx if leftward else sx + node_width
            x2 = tx + node_width if leftward else tx
            y1 = sy + node_height / 2
            y2 = ty + node_height / 2
            middle = (x1 + x2) / 2 + (index % 3 - 1) * 12
            points = [(x1, y1), (middle, y1), (middle, y2), (x2, y2)]
        kind = "conditional" if edge.conditional else "solid"
        color = "#b54625" if edge.conditional else "#526c75"
        dash = ' stroke-dasharray="9 6"' if edge.conditional else ""
        chunks.append(
            '<polyline points="'
            + " ".join((f"{x:g},{y:g}" for x, y in points))
            + f'" fill="none" stroke="{color}" stroke-width="3"{dash} marker-end="url(#{kind})"/>'
        )
        edge_data.append(
            {
                "source": edge.source,
                "target": edge.target,
                "conditional": bool(edge.conditional),
            }
        )
    for name in structure.nodes:
        x, y = layout[name]
        display = {"__start__": "START", "__end__": "END"}.get(name, name)
        highlight = name in {"review", "approval", "check"}
        chunks.append(
            f'<rect x="{x}" y="{y}" width="{node_width}" height="{node_height}" rx="12" fill="{('#f8e9df' if highlight else '#eaf0eb')}" stroke="#c9d6ce" stroke-width="2"/>'
        )
        chunks.append(
            f'<text x="{x + 14}" y="{y + 32}" font-family="Consolas,monospace" font-size="23" font-weight="bold" fill="#183443">{html.escape(display)}</text>'
        )
        chunks.append(
            f'<text x="{x + 14}" y="{y + 60}" font-family="Arial,sans-serif" font-size="15" fill="#526c75">{html.escape(ROLES.get(name, name))}</text>'
        )
    chunks += [
        f'<path d="M30 {height - 55} H{width - 30}" stroke="#d9dfd8"/>',
        f'<text x="30" y="{height - 25}" font-family="Arial,sans-serif" font-size="16" fill="#526c75">Solid: fixed edge. Orange dashed: conditional destination. See the routing explanation below.</text>',
        "</svg>",
    ]
    return (
        "\n".join(chunks),
        {"part": part, "nodes": list(structure.nodes), "edges": edge_data},
    )


def save_graph(graph, part):
    structure = graph.get_graph()
    svg, _ = draw_svg(structure, part)
    Path("workflow.svg").write_text(svg, encoding="utf-8")
    Path("workflow.mmd").write_text(structure.draw_mermaid(), encoding="utf-8")

Run it and compare the result

Run commands from the lesson folder. In the commands below, python means the environment you installed into. On Windows, substitute .venv\Scripts\python.exe; on Linux/macOS substitute .venv/bin/python. Do not create the environment with one interpreter and run the lesson with another.

Run 1: python lesson.py

python lesson.py

Captured standard output:

{
  "text": "  My Python module   is missing.  ",
  "cleaned": "My Python module is missing.",
  "reply": "Check the environment for this question: My Python module is missing."
}

The program also writes workflow.svg and workflow.mmd into the current folder. Open the SVG in a browser. The Mermaid file is a text representation of the same nodes and edges.

The displayed output was captured from the supplied default program in a separate folder, with no project-level module on its import path. It is a reproducible demonstration on these examples, not a claim about arbitrary inputs.

Walk through what just happened

1. Describe data with State

State describes the expected dictionary keys and value types. It contains no particular question. The actual starting dictionary is supplied to invoke at the end of the file. Optional fields are absent until a step returns them.

2. Return updates from clean and draft

The clean function reads text and returns only cleaned. Draft reads cleaned and returns only reply. For these ordinary fields, LangGraph applies the updates without deleting other keys. This is why the original text remains in the result.

3. Build the execution map

StateGraph creates a builder. add_node associates a string name with a Python function. add_edge places the functions between START and END. compile returns a runnable graph; it does not execute the nodes yet.

4. Invoke and inspect

invoke supplies the initial dictionary and runs the graph. The saved workflow.svg is generated from the compiled nodes and edges. Compare its clean → draft order with the order in which the result gains its fields.

Change one thing yourself

Change the input’s extra spaces, run the file again, and compare text with cleaned. Then explain why starting directly at draft would fail: cleaned has not been produced yet.

If your result is different

An import error for graph_picture means its complete file below was not saved beside lesson.py. A missing cleaned key usually means the graph ordering or the returned update changed.

Check the complete imports and filenames first. Then compare your input and command with the shown run. Numeric results may differ slightly between platforms; the program's assertions check the intended relationships rather than relying on a decorative screenshot.

What you can now explain

Build and execute a two-node graph, then explain every field in its final state. Next we will classify a question and choose between a technical reply and a human-review branch.

Technical reference: LangGraph Graph API. This project and its wording are original CodeTips Academy material.


Related Articles

Back to Articles