Primary sources
- Graph API overview · LangChain (LangGraph documentation) (accessed 09/19/2026)
- Dify · LangGenius (accessed 09/19/2026)
Describing a workflow as steps and the connections between them.
Sourced
Concept at a glance
The program owns the graph and the conditions that select its paths.
Same concept, different task and consequences. Switching starts a fresh walkthrough; prior answers and approvals do not carry over.
Representing a process as steps, transitions, and branches over state. Persistence can support recovery but is not required for a workflow to be a graph.
Follow a task through explicit branches, handoffs, and a review point. The example makes the route visible so you can inspect what happens when information is missing or a step must be repeated.
Prepare this week's portfolio report and send only after I approve its content and recipients.
A collection-to-review state graph, evidence links, unresolved-items queue, version-bound approval, and simulated duplicate-safe delivery.
The setting makes the example concrete. Carry the underlying pattern into your own work; adapt the sources, tools, and level of oversight to your task.
Prepare this week's portfolio report and send only after I approve its content and recipients.
Authored case. Select any record below; nothing is sent to a model.What changed: The current period starts with two supported updates and one explicit gap.
Transitions need defined conditions and state. A diagram alone does not ensure that a resumed or retried run behaves correctly.
Describe your task to your own model and use Workflow graphs as a reference. Ask whether it fits, which alternatives meet the same automation needs, and how you would implement and check the result.
A workflow graph describes a process as nodes and edges instead of nested code: each node does one unit of work, and each edge decides what runs next. LangGraph, a framework built around the idea, defines all three plainly. Of nodes: “Functions that encode the logic of your agents. They receive the current state as input, perform some computation or side-effect, and return an updated state.” Of edges: “Functions that determine which Node to execute next based on the current state.” Of state: “A shared data structure that represents the current snapshot of your application”[1]. State is passed from node to node rather than living in local variables.
The graph describes the available steps and transitions. A transition can use a deterministic condition or a model-produced classification; either way, the surrounding application defines how the result selects the next step. Persistence and checkpointing are additional capabilities, not requirements for something to be a workflow graph.
The distinction from agent graphs is the work inside the nodes and who chooses subsequent actions, not simply the presence of edges. A node can run a fixed operation or an agent loop, and a system can combine both. The site’s levels organize these patterns for explanation; they are not universal framework categories. It is also the middle page of the site’s graph engineering thread: knowledge graphs connect information, agent graphs connect work, and a workflow graph is what you draw when the work is settled in advance.
This page provides primary references and illustrative examples. The examples are scripted, not measured model runs; source references do not establish the correctness of every implementation or outcome.
This separate, scripted trace illustrates the code example discussed in the implementation details. Use it to inspect individual steps, inputs, and outputs.
Nodes are functions over shared state; edges are code that reads the state and picks the next node.
This is a scripted illustration, not a recorded model run. Its timing and token figures are not measured performance.
"How often should the DW-300's filter be cleaned?"
Build a small workflow graph in a visual automation canvas: a trigger, a sequence of steps, and at least one branch node that sends the run one way or another depending on something in the data so far, such as sending it one place if an amount is over a threshold and somewhere else if it is not. Dify describes the appeal as a visible plan rather than a hidden prompt: “visual building blocks and AI prompts to define how an app thinks, retrieves data, makes decisions, uses tools, asks for human input, and completes tasks,” turning “Prompt Logic Into a Visible Execution Path”[2].
A branch is worth trusting only once you know what actually decides it. A rule, such as one that checks whether a category field equals a fixed value, is fully predictable: build one input for each path on purpose and confirm each one lands where you expect. A branch that asks a model which way to go, inside that same node, wears the identical visual shape but behaves differently: you cannot enumerate every path in advance the way you can with a rule, so test it with several inputs worded differently but aimed at the same branch, not just one.
After a run finishes, open its history if the tool keeps one and read what each node received and returned, in order, not only the final output. If a run failed or took the wrong branch, that per-node history is where the wrong turn actually shows up. And if a run fails partway through, check whether the tool can resume from where it stopped instead of starting the whole thing over; that depends on whether it actually saved the state at each step along the way, which is usually a setting worth turning on rather than something you get for free.
A graph is more machinery than a task needs when nothing in it ever branches. A plain ordered list of steps does that job with one less thing to build, test and reread later, and is worth reaching for first.
The example is the same draft/check/revise loop as write and check, run through a small graph
executor instead of a hand-written loop, plus one branch a single loop does not express as
cleanly: if retrieval finds nothing, the graph goes straight to a dead end (no_match) instead
of drafting from zero sources.
A node is a plain function of the shared state that returns an updated state, and nothing else.
This one is the whole of retrieve:
def _node_retrieve(state: State, sections: dict[str, Section], model: Model) -> State:
hits = [s for s, score in bm25_search(sections, state["question"], k=RETRIEVE_K) if score > 0]
state["source_cites"] = [s.cite for s in hits]
return stateNODES collects five of those under their ids; EDGES holds one function per node, each
reading the state and returning the id of the next node, or None to stop. The runner is one
while loop: run the current node, record what it did, ask its edge function what comes next,
write a checkpoint, move on. The draft, check and revise nodes are the same
prompts write and check uses, so they are
left out below; the graph machinery is the part worth reading here.
NODES: dict[str, Callable[[State, dict, Model], State]] = {
"retrieve": _node_retrieve,
"no_match": _node_no_match,
"draft": _node_draft,
"check": _node_check,
"revise": _node_revise,
}
def _edge_from_retrieve(state: State) -> str | None:
return "draft" if state["source_cites"] else "no_match"
def _edge_from_check(state: State) -> str | None:
if state["verdict"] == "ok" or state["revisions"] >= MAX_REVISIONS:
return None
return "revise"
EDGES: dict[str, Callable[[State], str | None]] = {
"retrieve": _edge_from_retrieve,
"no_match": lambda state: None,
"draft": lambda state: "check",
"check": _edge_from_check,
"revise": lambda state: "check",
}
def run(
question: str,
model: Model,
embedder: Embedder | None,
tracer: Tracer,
*,
corpus_dir: Path = DEFAULT_CORPUS_DIR,
) -> Answer:
del embedder # retrieval here is keyword search, not a vector index
sections = load_sections(corpus_dir)
state: State = {"question": question, "source_cites": [], "draft_text": "", "verdict": None, "revisions": 0}
node_id: str | None = "retrieve"
while node_id is not None:
state = NODES[node_id](state, sections, model)
completion = state.pop("_completion", None)
if completion is not None:
tracer.record(
kind="model", decided_by="code", title=f"Node: {node_id}", detail=completion.text[:200],
tokens_in=completion.tokens_in, tokens_out=completion.tokens_out, ms=completion.ms,
)
else:
tracer.record(kind="code", decided_by="code", title=f"Node: {node_id}", detail=", ".join(state["source_cites"]) or "none")
next_id = EDGES[node_id](state)
tracer.record(
kind="code", decided_by="code", title=f"Checkpoint after '{node_id}'",
detail=f"revisions={state['revisions']} verdict={state.get('verdict')!r} -> next: {next_id or 'stop'}",
)
node_id = next_id
citations = cited_sources(state["draft_text"])
return Answer(text=state["draft_text"], citations=citations, retrieved_sources=state["source_cites"])The checkpoint is the concrete payoff graphs are usually sold on. state’s durable fields are
all plain values (strings, an int, a list of citation strings) so recording it after every node
is a real json.dumps, not an idea that would need a redesign to actually implement. A crashed
run could resume from the last written checkpoint by loading that same dict and re-entering the
loop at the node the checkpoint names, with no change to NODES or EDGES at all.
Whether the graph is worth it here is a fair question, and the honest answer is: barely, for
this exact example. Five nodes and a handful of edge functions do the same job
write-and-check’s plain while loop does in fewer lines, with one extra indirection (looking a
function up in a dict instead of calling it directly) that buys nothing when there is only one
reasonable order to run things in. The place a graph earns that cost back is where this example
starts to gesture at it but does not fully need it: a genuine branch (no_match) that a nested
if inside a longer function would have buried, a state shape simple enough to checkpoint for
real, and node functions that would still make sense wired into a different graph: none of
which a straight-line function forbids, but a graph makes structurally obvious rather than
something a reader has to reconstruct from control flow.
Run it yourself:
python -m examples.workflow_graphs --model stub:scriptedTry a plain function with an ordinary if/while, the way write and check’s example does, first if the
process has one obvious order and no real branch: a graph’s nodes-and-edges indirection is
ceremony when there is only one path through the code anyway.
Move up to a workflow graph once the process has a real branch that a nested if would bury, a
state shape worth checkpointing between steps, or node functions you expect to reuse in more
than one wiring.
Illustrative, not measured: no result file exists for this technique yet, so every number below is a worked example. See docs/EVALS.md for how a real run is recorded and scored.
The same 60 questions as every other technique, with a narrow interest in the result. Because
the graph runs the same logic as write and check, its score on citation hit rate and revision
count should match
that page’s result file once one exists: a divergence between the two would point at a bug in
the graph wiring, not a difference in what the technique can do. The no_match branch is what
unanswerable questions specifically exercise: whether retrieval finding nothing correctly ends
the run at a dead end rather than drafting from an empty source list.
No result file exists yet (see docs/EVALS.md). Run python scripts/eval_run.py --example workflow_graphs --model <spec> --dry to project the cost of a real run first.
Which node a run stopped on and why (an edge function's return value), not just the final answer. A node that is visited far more or less often than expected is a sign a branch condition drifted from what real traffic actually looks like.
The same as whatever the underlying nodes cost, since the graph runner itself makes no model calls of its own. The checkpoint write after every node adds a small, fixed storage cost per node visited, independent of question difficulty.
An edge function's condition stops matching reality (a state field's shape changed upstream and the comparison silently always takes the same branch), which nothing catches unless every edge function is tested against the specific states it is meant to branch on.
The full state dict at every checkpoint, and which edge each transition took, not just the final node's output. A wrong final answer is only debuggable if you can see the exact state the run was in when it decided each turn.
Find an automation tool with a visual if/else node. Read one branch condition: a fixed rule, or a model deciding?
Run python -m examples.workflow_graphs --model stub:scripted from the repo root: retrieve, draft, a check that fails on an unretrieved citation, revise, check, stop, checkpointing after every node. Now ask a question of gibberish words: retrieval returns nothing, the graph goes straight to no_match, and two checkpoints stand in for five.
Draw a process you run as nodes and edges, then count the edges whose condition you could not write as a line of code. That is where it stops being this level.
5 current examples · Products, tools, and models that demonstrate this concept. A selection, not a ranking.
Graph framework
Maker’s documentation Checked 09/19/2026Automation service, self-hostable
Maker’s documentation Checked 09/18/2026Visual workflow builder
Maker’s documentation Checked 09/18/2026Visual workflow builder
Maker’s documentation Checked 09/18/2026Visual workflow builder
Maker’s documentation Checked 09/18/2026Connect extraction, validation, review, and completion steps with explicit success and failure paths.
Names listed 09/19/2026. 223 of 223 registry entries have been checked against the maker's own page; the registry marks the rest as unchecked.
Last reviewed 09/19/2026. Pages unreviewed for 90 days are flagged for another pass. Markdown version of this page