AI Workflows

Part 3 — Route Support Questions with Conditional Edges and State Updates

CodeTips Academy
October 24, 2026
12 min read
64 views
Route Support Questions with Conditional Edges and State Updates

Give technical questions and human-only requests different paths, and keep a readable log of the nodes that ran.

Complete code for Part 3: 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

A missing Python package and a refund request should not follow the same automated route. We will first label the kind of question, then choose the next graph node from that label. Keeping those two decisions separate makes the program easier to inspect.

Everything is still rule-based. The complete file includes the word tokenizer and classifier rather than importing an unexplained project helper. It runs two requests so you can compare the technical and human-review paths side by side.

By the end: Route technical questions and refund questions differently, while retaining an ordered event log.

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

The state is this ticket's dictionary of data. A node is a Python function registered as a workflow step. An edge chooses a possible next step. A compiled graph runs those steps. In this part, the graph will choose one of two routes instead of always executing a straight line.

Classification answers “what kind of question is this?” Our classify_rules function inspects words and returns a category string such as "setup" or "billing". It is a programmed rule, not a trained classifier. Routing answers a different question: “given that category, which node should run next?” The route function returns the name "technical" or "human".

For a question about a missing Python module, the category is setup and the route is technical. A request for a refund goes to human review. “Human” here means the program stops on that branch with a review message; the lesson does not automatically contact a real mentor.

An event is a short log entry such as "classify:setup", recording what happened. A reducer is the function used to combine a field's existing value with a returned update. Without a special reducer, an ordinary field update replaces that field's value.

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
import re


def tokens(text: str) -> set[str]:
    return set(re.findall("[a-z0-9]+", text.lower()))


def classify_rules(text: str) -> str:
    words = tokens(text)
    if words & {"refund", "payment", "invoice", "charge", "billing"}:
        return "billing"
    if words & {"esp32", "port", "serial", "usb", "board", "camera"}:
        return "hardware"
    if words & {"python", "pip", "venv", "install", "module", "import", "environment"}:
        return "setup"
    return "other"


"Article 3: choose a branch and append an event log."
import json
from operator import add
from typing import Annotated, TypedDict
from langgraph.graph import END, START, StateGraph


class State(TypedDict, total=False):
    text: str
    category: str
    reply: str
    events: Annotated[list[str], add]


def classify(state):
    category = classify_rules(state["text"])
    return {"category": category, "events": [f"classify:{category}"]}


def route(state):
    return "technical" if state["category"] in {"setup", "hardware"} else "human"


def technical(state):
    return {
        "reply": "Next we need a relevant workshop document.",
        "events": ["technical"],
    }


def human(state):
    return {"reply": "A mentor must review this request.", "events": ["human"]}


def build():
    builder = StateGraph(State)
    for name, fn in [
        ("classify", classify),
        ("technical", technical),
        ("human", human),
    ]:
        builder.add_node(name, fn)
    builder.add_edge(START, "classify")
    builder.add_conditional_edges("classify", route, ["technical", "human"])
    builder.add_edge("technical", END)
    builder.add_edge("human", END)
    compiled = builder.compile()
    save_graph(compiled, 3)
    return compiled


if __name__ == "__main__":
    for text in ["My Python module is missing", "Please refund my Python workshop"]:
        print(json.dumps(build().invoke({"text": text, "events": []}), 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",
  "category": "setup",
  "reply": "Next we need a relevant workshop document.",
  "events": [
    "classify:setup",
    "technical"
  ]
}
{
  "text": "Please refund my Python workshop",
  "category": "billing",
  "reply": "A mentor must review this request.",
  "events": [
    "classify:billing",
    "human"
  ]
}

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. Read the imports and tokenizer

The file explicitly imports re before calling re.findall. tokens lowercases the text and returns a set of word-like items. A set intersection finds which listed keywords occur in the request.

2. Classify before routing

classify_rules checks billing keywords before technical ones. Consequently, a refund request containing the word Python is still billing. The classify node stores that category; route reads it and returns the next node name.

3. Combine new events correctly

The events field uses Annotated with operator.add as its reducer. New lists are concatenated. Each node therefore returns only its own new log entry; returning the accumulated list would duplicate entries.

4. Read one execution at a time

The diagram contains both possible branches, but a particular input follows one of them. In each printed result, compare category, reply, and events. The event list is a concise execution trace, not hidden model reasoning.

Change one thing yourself

Add “Please refund my ESP32 workshop” as another input. The billing-first rule should send it to human. Explain the result from the classifier’s order, not from a guess about AI behavior.

If your result is different

If only the latest event remains, check the reducer declaration. If events repeat, check that nodes return only their new entries.

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

Route technical questions and refund questions differently, while retaining an ordered event log. Next we will look up relevant workshop notes and stop a draft-check loop after a fixed number of attempts.

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


Related Articles

Back to Articles