1:1 mentoring with Big Tech AI engineers
Back to blog
By AgenticPrep Team14 min read

Graph Engineering — Designing What the Agent Is Allowed to Do Next

A loop asks the model what to do next on every turn. A graph writes down the moves that are allowed and lets the model choose inside them — which is how an agent stops being a demo and starts being something you can trust with a refund.

graph-engineeringlanggraphagentsorchestration

A loop asks the model “what should I do next?” on every turn. A graph writes down the moves that are allowed and lets the model choose inside them — and that single change is what turns a demo agent into something you can debug, resume, and trust with a refund.

The first support agent I put in front of real customers issued the same $80 refund twice. Not because the model was dumb — because the run crashed after the refund call and the retry started from the top, re-read the ticket, reached the same conclusion, and paid out again. There was no record of where it had got to, because there was no “where”: the whole agent was a while loop with a list of tools, and its only memory of progress was a message history that the retry threw away.

The fix wasn’t a better prompt or a bigger model. It was drawing the thing. Once the agent was an explicit graph — classify, check, decide, gate, execute — two of the three holes closed on their own: execute became a node you can only reach through gate, and a crashed run picked up at the node it had finished instead of at the top. The third hole — making the payout itself safe to replay — stayed my job, and I’ll come back to it. But it went from a mystery to one bounded problem with a name.

That practice has a name now too: graph engineering. It’s the discipline of designing an agent’s control flow as an explicit graph — nodes that do work, edges that decide what runs next, and one state object that threads through both — instead of letting the flow emerge from whatever the model felt like doing this time.

THE REFRAME

Prompt engineering asks “what do I say?” Context engineering asks “what should be in the window?” Graph engineering asks “which steps exist, and what is allowed to follow what?” The answer stops being a paragraph in a system prompt and becomes a data structure you can read, diff, test, and draw on a whiteboard.

Chain, loop, graph

Three shapes cover almost every agent people ship. They’re not competing philosophies — they’re different amounts of structure, and the right one depends on how much of the flow you actually know in advance.

The same work, three shapes — and where the control lives in each
CHAIN you wrote the path step 1 step 2 step 3 END LOOP the model picks each step model tools GRAPH you own the map classify refund answer END refund? else Same work, three shapes — a graph is the same autonomy, written down where you can inspect it.
ShapeWho decides what runs nextGood atFalls over when
ChainYou, at write timeKnown pipelines: extract → summarize → formatAnything needs to branch, retry, or wait
LoopThe model, every turnOpen-ended work where the steps aren’t knowableYou need a guarantee — approval, budget, ordering
GraphYou define the options; state or the model picks oneReal workflows: branches, parallel checks, gates, retriesThe task is genuinely one straight line (then it’s overhead)

The three parts

Every graph framework is built on the same three ideas. The code here is LangGraph; learn the ideas and the API is a detail — there’s a survey of what else implements this shape near the end.

One run: nodes do the work, state carries it, edges choose the next node
ONE RUN OF A SUPPORT GRAPH START classify one LLM call lookup_order plain function approval_gate waits for a human refund side effect END THE STATE, AFTER EACH NODE ticket: "..." + intent: refund + order: A-91 + amount: 80 + approved: true + refund_id THE EDGE THAT DOES THE WORK classify → where_next(state) → refund | tracking | anything else a pure function of state — unit-testable, reviewable in a diff, free to run State is what moves. Nodes are what happens. Edges are what you actually design.

State — the one object that moves

A typed dict every node reads from and returns updates to. Nodes never mutate it; they return a patch and the framework merges it. That’s what makes a node replayable.

Nodes — the units of work

A node is just a function. Some call a model, most don’t: a database lookup, a validation, a Stripe call, another whole graph. If a step can be plain code, make it plain code — deterministic nodes are the cheap, fast, testable parts of your agent.

Edges — what may follow what

A fixed edge always goes to the same place. A conditional edge runs a function over the state and returns the name of the next node. That function is your routing policy, and it lives in version control instead of in a paragraph of prompt.

Here’s the whole idea in twenty-odd lines — a ticket comes in, gets classified, gets answered:

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class State(TypedDict):
    ticket: str
    intent: str
    reply: str

def classify(state: State) -> dict:
    return {"intent": intent_of(state["ticket"])}     # LLM call, or a rule

def answer(state: State) -> dict:
    return {"reply": draft(state["ticket"], state["intent"])}

builder = StateGraph(State)
builder.add_node("classify", classify)
builder.add_node("answer", answer)
builder.add_edge(START, "classify")
builder.add_edge("classify", "answer")
builder.add_edge("answer", END)
graph = builder.compile()

graph.invoke({"ticket": "where is order A-91?"})
# {'ticket': '...', 'intent': 'tracking', 'reply': 'It shipped Tuesday...'}

That’s still a chain — a straight line. It becomes a graph the moment one edge starts making a decision.

The engineering lives in the edges

Nodes are the boring part. Anyone can write a function that calls a model. The design work — the part that decides whether your agent is trustworthy — is in what you allow to follow what.

def where_next(state: State) -> str:
    # a pure function of state: no model call, no I/O, trivially testable
    if state["intent"] == "refund":   return "lookup_order"
    if state["intent"] == "tracking": return "shipping"
    return "escalate"

builder.add_conditional_edges(
    "classify",                                   # the node we are leaving
    where_next,                                   # what decides
    ["lookup_order", "shipping", "escalate"],     # where it may go
)

That third argument is the map from what the function returns to where the run goes. Return node names, as here, and a plain list is enough. Return abstract labels instead — "too_big", "ok" — and you pass a dict that maps each label to a node, which is worth doing once the label and the node stop meaning the same thing.

Now look at what you got for free. Routing is a function you can run over 200 past tickets in a unit test, with no model in the loop. A reviewer can see, in a diff, that escalate was added as the default. And refund can no longer happen from anywhere — there is exactly one path to it, and it goes through the gate.

TIP

When a routing decision needs the model’s judgement, keep the model inside a node that writes its verdict into state, and let the edge read that field. Edges stay pure and cheap; the model’s decision becomes a value you can log, evaluate, and replay.

Four patterns you’ll use again and again

Nearly every production graph is a mix of these four. They’re worth knowing by name, because once you see the shape you stop reinventing it.

The four edge patterns — router, fan-out/join, guarded cycle, human checkpoint
1 · ROUTER route billing shipping escalate one edge, three exits — state picks one 2 · FAN-OUT / JOIN split fraud stock policy join three at once — a reducer merges what they write 3 · GUARDED CYCLE draft verify END pass fail — retry, max 3 a loop with a written exit — the guard is the point 4 · HUMAN CHECKPOINT propose gate execute paused — state saved to disk the run parks; a human resumes it hours later

1 · Router

One node, several possible successors, a function that picks one. This is the pattern from the last section — the workhorse, and usually the first edge you write.

2 · Fan-out and join

Three independent checks have no reason to run one after another. Point three edges out of one node and they run in parallel; point them all at one successor and it runs once, after the slowest finishes. The catch: three nodes now write to the same state at the same time, so any field they share needs a reducer — a merge rule — or the last writer silently wins.

3 · Guarded cycle

Draft → verify → draft again is how you get self-correction. A cycle without a counter is how you get a bill. Every cycle needs a written exit: attempts, budget, or wall-clock.

4 · Human checkpoint

A node that stops mid-run, hands a payload to a person, and continues from that exact spot when the answer comes back — even if that’s tomorrow, from a different process. This is the pattern that makes an agent safe to point at money; how a paused run survives that wait is the next section.

The fan-out reducer is the one that bites people, so it’s worth seeing:

import operator
from typing import Annotated, TypedDict

class State(TypedDict):
    findings: Annotated[list, operator.add]   # concatenates: parallel-safe
    risk:     str                             # no reducer: last write wins

for check in ("fraud", "stock", "policy"):
    builder.add_edge("split", check)          # all three start together
    builder.add_edge(check, "decide")         # decide runs once, after all three

def fraud(state: State) -> dict:
    return {"findings": ["fraud: clean"]}     # appended, not overwritten

And the guard on a cycle. The counter is the whole trick, so the node that retries is the node that has to increment it:

def draft(state: State) -> dict:
    return {"reply": write(state), "attempts": state["attempts"] + 1}

def after_verify(state: State) -> str:
    if state["passed"]:          return "done"
    if state["attempts"] >= 3:   return "human"     # give up honestly
    return "retry"

builder.add_conditional_edges("verify", after_verify,
    {"done": END, "retry": "draft", "human": "escalate"})

Durability falls out for free

Here’s the part that fixed my double refund. Because the graph knows what a “step” is, the framework can save the state after every node — a checkpoint — keyed by a thread id you choose. Crash after refund and the retry resumes after refund, not from the top.

The same mechanism gives you the human checkpoint: pausing is just a checkpoint that nobody has resumed yet.

from langgraph.types import interrupt, Command
from langgraph.checkpoint.postgres import PostgresSaver

def approval_gate(state: State) -> dict:
    decision = interrupt({                      # whatever your UI needs to show
        "action": "refund", "order": state["order"], "amount_usd": state["amount"],
    })
    return {"approved": decision == "approve"}

with PostgresSaver.from_conn_string(DB_URL) as cp:
    cp.setup()                                  # creates the tables, once at deploy
    graph = builder.compile(checkpointer=cp)    # without this it can pause, never resume

config = {"configurable": {"thread_id": "ticket-8412"}}
graph.invoke({"ticket": "refund my order A-91"}, config)
graph.get_state(config).next          # ('approval_gate',) - parked, nothing lost

# ...two hours later, a different process, same thread_id
graph.invoke(Command(resume="approve"), config)
WATCH FOR

A node that is not safe to run twice — the third hole from the opening story. Checkpointing means a node can be replayed after a crash, so anything with a side effect — charging a card, sending mail, writing a row — needs an idempotency key derived from state. “Resumable” and “idempotent” are not the same promise, and the gap between them is exactly where duplicate refunds live.

Worked example: the refund graph

Put the four patterns together and you get something that looks like a real system, because it is one. Same task as the opening story — refund a customer — drawn instead of improvised.

One topology, five outcomes — and every one of them is a path you can point at
THE REFUND GRAPH START classify one LLM call fraud_check stock_check policy_check decide waits for all three auto_refund under the limit approval_gate waits for a human escalate no payout execute idempotent END all three in parallel < $50 ≥ $50 a check failed Five outcomes, one picture — and the picture is the code, not a drawing of it.

Four things in that drawing are what separate it from a diagram someone sketched once and left behind:

What you addWhat it buys you
A checkpoint after every nodeA crash at execute resumes at execute, not back at step one
One thread id per ticketThe gate can wait two days, and the run survives a deploy in between
A routing function, not a promptReplay 200 past tickets through the router in a test, with no model calls
A retry edge with a capTwo tool timeouts get absorbed; the third escalates instead of looping

The loop version

One while loop, eight tools, a 900-word system prompt asking it to please get approval for refunds over $50. It usually does. When it doesn’t, the log is a wall of messages and the answer to “why did it refund without asking?” is we think the instruction got lost around turn 14. A crash mid-run starts over. Checks run one at a time: 6–8s per ticket.

The graph version

Refunds over $50 cannot reach execute without passing approval_gate — not as an instruction, as a topology. The three checks run in parallel: ~2s. A crash resumes at the last completed node. “Why did it escalate?” is answered by one line in the checkpoint history, and the routing rule that produced it is eight lines of Python in a diff.

When not to reach for a graph

Graphs are not free. You pay in a state schema, a framework, and a mental model your teammates have to learn. Skip it when:

  • It’s genuinely a straight line. Extract → summarize → post has no branches. Write the three function calls.
  • The steps really aren’t knowable. Open-ended research is a loop with good tools. A graph whose every edge says “ask the model” is a loop wearing a costume — and a slower one.
  • You’re still finding the shape. Prototype as a loop, watch where it actually goes, then draw the graph you observed. Designing the topology before you’ve seen one real run is how you get twelve nodes where four would do.
THE OVER-ENGINEERING SMELL

Nodes that exist only to pass state to the next node. Conditional edges with one destination. A state object with fourteen fields where four are ever read. If your graph diagram needs a legend, you’ve modelled your org chart instead of your workflow — collapse it until every node earns its box.

The shape outlives the framework

Every example here is LangGraph, because it’s what most people meet first. But nothing in the last four sections is LangGraph’s idea. Explicit states, allowed transitions, retries with a cap, a step that waits for a human — workflow engines were doing all of that a decade before anyone put a model inside a node. What’s new is only that some of the nodes now call one.

Where you’ll meet the shapeWhat it calls the piecesHow explicit the topology is
LangGraphState, nodes, edges, checkpointerFully — the graph is the artifact you write and review
OpenAI Agents SDKAgents and handoffsControl passes agent to agent and context rides in the conversation, not a typed state object — fewer primitives, less written down
Role-based frameworks (CrewAI and kin)Agents with roles and goalsImplied by descriptions — quick to start, hard to point at when it misroutes
Durable execution (Temporal, Restate, DBOS)Workflows and stepsThe checkpoint half without the model half — often paired with one of the above
Step Functions, BPMN enginesStates and transitionsThe same shape, pre-LLM, with retries and approval steps already in the box

Choosing between them is a real decision, and “most explicit” isn’t automatically right — a handoff framework gives you less to configure, which is the better trade when the steps genuinely aren’t knowable. What ports is the three questions: what are the nodes, what is allowed to follow what, and where does a run park when something has to wait? Answer those and you can carry the design to whatever you use next quarter. Skip them and no framework saves you.

THE SAME TRAP EVERYWHERE

The durable-execution engines land on exactly the caveat from earlier. Temporal replays a workflow’s decisions exactly once, but the work those decisions trigger is at-least-once by default — so anything with a side effect still needs an idempotency key. That isn’t a LangGraph quirk you can switch tools to escape. It’s what “resumable” costs, in every system that sells it.

How to draw your first one

The fastest path from “I have an agent” to “I have a graph”, in the order that actually works:

  • 1 · List the decisions, not the steps. Every place the flow can go two ways is an edge. Everything between two decisions is a node.
  • 2 · Write the state first. Four or five fields. If you can’t name what moves between nodes, you don’t have the shape yet.
  • 3 · Make every step you can deterministic. Lookups, validation, formatting — plain functions. Reserve model calls for judgement.
  • 4 · Put a gate in front of anything irreversible. Money, email, deletes. A human checkpoint costs one node.
  • 5 · Add a checkpointer on day one. Not when it breaks in production — you’ll want the run history the first time you debug a route.
THE TAKEAWAY

The four disciplines stack. The harness decides what the model can touch; context engineering decides what it sees; loop engineering decides how the whole thing repeats; graph engineering decides what is allowed to happen next. It’s the layer that turns “the agent usually does the right thing” into “the agent cannot do the wrong thing” — and that sentence is the entire difference between a demo and production.

If you’re preparing for an FDE or AI engineering interview, this is worth practising out loud: given a workflow, name the nodes, name the edges, say where the checkpoint goes and why. It’s the fastest way to show you’ve shipped one.

GO DEEPER

LangChain, “3 Years of Graph Engineering with LangGraph” — how the framing emerged from real deployments · the StateGraph reference for the edge API used above · Analytics Vidhya, “Graph Engineering for AI Agents: A Complete Guide in LangGraph” · Anthropic, “Building effective agents” — the workflow patterns these topologies formalise · our companion posts on harness, context and loop engineering.

Enjoyed this post? The full curriculum has 74+ sections, system design problems, and AI-reviewed practice runs.

See the full guide