Anshad Ameenza.
Engineering··Updated: Jul 19, 2026

Graphs and State Machines for Agents, Part 1: From Lines to Graphs

Most multi-step agents are a straight line that waits on itself. Model the work as an AI agent graph of nodes and edges and it collapses into a wider shape.


Give an agent a real job and time it. Say the job is to review a pull request that changed four things: a style pass, a type check, a test run, and a written verdict at the end. A typical agent does the style pass, thinks, does the type check, thinks, runs the tests, thinks, and about a minute later hands you the verdict. It felt busy the whole time. Now ask the one question that matters. Did the type check need anything the style pass produced? It did not. Did the test run wait on the type check for a reason? No. The agent stood in its own queue three times and charged you for the wait.

This is the first piece of a five-part series on building agents that do not do that. Across these parts we go from this observation to a full working discipline: this part draws the shape, Part 2 shows that the shape is a finite state machine, Part 3 builds the machine, Part 4 makes agents reliable by construction, and Part 5 runs the whole thing across frontier and open-weight models. It all starts with one claim.

The work an agent does has a shape, and that shape is almost never a line. It is a graph.

The shift almost nobody has named

Watch how people talk about agents and you hear a verb: the agent does a sequence of steps. First this, then that, then the next thing. The mental model is a to-do list the model walks top to bottom. That model is not wrong so much as it is a trap, because it smuggles in an assumption you never checked: that each step depends on the one above it. Most of the time it does not.

Strip the mystique out of “agent orchestration” and you are left with exactly two objects. Nodes and edges. That is the whole vocabulary, and once you have it you stop seeing a list and start seeing a graph. This is the backbone under tools like LangGraph and Jerry Liu’s LlamaIndex Workflows, and it is the same shape decades of dataflow and build systems have used, because computation has always had a shape. The model does not get to pretend otherwise.

Here is the line I want you to carry through all five parts, because it is the difference between the two ways of working: a prompter asks a question; an architect draws a graph. The prompter improves an agent by asking better, longer, more careful questions in sequence. The architect improves it by drawing a better graph. Everything below is how you become the second person.

A node is a job

A node is one job. One agent call, or one plain function, with a bounded input going in and one output coming out. It classifies a ticket. It extracts a field. It runs the tests. It writes the verdict. It does exactly one thing and returns. That is the entire definition, and its strictness is the point: if you cannot say in one sentence what a node takes and what it returns, it is not one node, it is several wearing a trench coat.

The style pass in our review is a node. Its input is the diff. Its output is a list of style findings. The type check is another node with the same input and a different output. The verdict is a node too, but a different kind: its input is not the diff, it is the three findings lists, and its output is the prose you actually wanted. Same object, different wiring. Which brings us to the wiring.

An edge is a data dependency, and “and then” is not one

An edge is a data dependency. It exists only when this node’s output actually feeds that node’s input. That word actually is carrying the whole argument, so slow down on it.

When you wrote the agent as a line, you connected every step to the next with the word “and then.” Style, and then types, and then tests, and then verdict. But “and then” is just the order you happened to type. Typing order is not a dependency. The type check would produce the identical result whether the style pass ran before it, after it, or never. Three of those four arrows carry nothing but the sequence in which the words left your keyboard, and sequence is not data.

An edge is a value crossing from one call’s return into another call’s prompt. If no value crosses, there is no edge, only the accident of the order you typed.

The one distinction the whole series rests on

Be precise here, because this is the exact place the wall-clock time hides. An edge is a real constraint on time: if B genuinely needs A’s output, B cannot start until A finishes, and that wait is unavoidable. A fake edge is a wait you invented by writing a for loop where a set would have done. The independent nodes could all have run at the same instant, and every second one of them spent waiting on another was a second you burned for no reason and no result.

The arrow test

So here is the test, and you should run it on every arrow you think you have.

Can you name the variable that crosses the arrow? A specific value, returned by one call, passed into the next call’s prompt or input. If you can name it, the edge is real and the wait is honest. If you cannot, the two nodes are independent, and independence is the entire thing you are about to exploit.

Run the test on our four-step review and it fails three times out of four. Style to types: no variable crosses, fiction. Types to tests: no variable crosses, fiction. Tests to verdict: testResults crosses, real. And separately, style to verdict and types to verdict are both real, because the verdict node reads all three findings. The true dependency structure was never a line. It was three independent jobs feeding one job that needs them together. You just could not see it because you wrote it down one line at a time.

A straight line is the saddest graph there is

Notice what this makes a linear agent. It is not the opposite of a graph. It is a graph, the most degenerate one possible: every node has exactly one edge in and one edge out. A single strand with no width anywhere.

That topology is correct. It will produce the right answer. It is also slow and fragile, and the fragility follows directly from the shape. A chain has no redundancy. If the type check stalls, the tests never run, and the style findings that node one already produced sit trapped upstream with nowhere to go. One slow link and the whole thing hangs, holding hostage all the work that finished before it. You did not design that fragility. You inherited it the moment you accepted the line.

The line: four steps, three of the arrows carry only typing orderstyletypestestsverdictfictionfictionrealThe graph: three independent checks, one merge that needs all threestyletypestestsverdict
The same four-step review, drawn twice. As a line it pays the sum of every step and hangs if one link stalls. Redrawn on its real dependencies it pays only the slowest of the three independent checks, plus the verdict.

The first skill is not building. It is redrawing.

Here is the surprising part. The first skill of thinking in graphs does not involve building anything new. It is redrawing what you already have.

Take your chain and walk every arrow with the arrow test. For each one, ask whether a variable really crosses. Delete every arrow that carries only the order you typed. What is left is the true shape, and it is almost always wider than the line you started with: a cluster of independent nodes that could all run at once, feeding the one node that genuinely needs them together. You did not add capability. You revealed the capability that was hiding under a false sequence.

In code the redraw is almost embarrassingly small. The before is the line you would write without thinking.

Before: the line that waits on itself

Four awaits in a row. Each one blocks the next, even though only the last actually depends on the first three. The three checks stand in a queue they never needed to join.

// Illustrative. A four-step review written as a line.
async function review(pr) {
  const style = await checkStyle(pr);   // step 1
  const types = await checkTypes(pr);   // step 2  (needs nothing from step 1)
  const tests = await runTests(pr);     // step 3  (needs nothing from step 2)
  return writeVerdict(pr, style, types, tests); // step 4 needs all three
}

After: the same work, redrawn on its real edges

Run the arrow test, find three fictional arrows, and delete them. The three checks fire together; only the verdict waits, because only the verdict has a real edge. Same model, same tokens, same result. The wall-clock drops from the sum of the three checks to the slowest single one.

// The identical work as a graph: three independent nodes, then the merge.
async function review(pr) {
  const [style, types, tests] = await Promise.all([
    checkStyle(pr),   // no variable crosses
    checkTypes(pr),   // between these
    runTests(pr),     // three nodes
  ]);
  return writeVerdict(pr, style, types, tests); // the one node that needs them all
}

Two lines of difference. The Promise.all is not a performance trick you sprinkle on at the end. It is you writing down the shape the work actually had. This series will get to much richer graphs than this, with branches and merges and loops, but every one of them starts from the same move you just made: draw the arrows, keep the real ones, delete the fictions.

The node contract: bounded in, validated out

A graph runs reliably only when its nodes are honest about what they promise, so before you widen anything, tighten the node. A node’s contract has two halves, and both matter more than they look.

The input is bounded and passed explicitly. You hand the node exactly the data it needs, as an argument, and nothing else. The failure mode this avoids is the shared context window: a design where every node reads from one big conversation that some other node might have written to, so a node’s behavior depends on what happened elsewhere in a way you cannot see or test. A node that reads only its arguments is a node you can run in isolation, reason about, and trust. A node that reads the ambient soup is a node that works until the day another node poisons the soup.

The output is validated against a schema. The node returns a defined shape, not a paragraph of prose the next node has to parse and pray over. This is the difference between an edge that carries { severity: "high", file: "auth.ts" } and an edge that carries “I found a few issues, the most serious being in the auth file.” The first is data. The second is a coin flip. Pin the output with a schema and the downstream node consumes a shape it can rely on instead of doing natural-language archaeology at three in the morning.

A node with a validated output

The schema is the contract. Structured output is checked before the node returns, so the edge out of this node always carries a known shape. The next node never parses free text.

// Illustrative. A node is one agent call with a schema on the way out.
const FindingSchema = {
  type: "object",
  required: ["severity", "file", "summary"],
  properties: {
    severity: { enum: ["low", "medium", "high"] },
    file: { type: "string" },
    summary: { type: "string", maxLength: 200 },
  },
  additionalProperties: false,
};

async function checkStyle(pr) {
  return model.respond({
    prompt: `Review this diff for style issues:\n${pr.diff}`,
    schema: FindingSchema,  // validated before it returns
  });
}

The schema does quiet double duty. It makes the node’s output trustworthy today, and it makes the edge legible tomorrow, because now anyone reading the graph can see exactly what travels down the arrow. A node that returns prose is a node that will eventually return prose in the wrong format, and the graph will break somewhere downstream where the cause is hard to find. A node that returns a validated shape fails loudly, at the node, where the fix is obvious.

The edge contract: name it by its data, and let plain code do the plumbing

If a node has a contract, so does an edge, and its contract is even simpler. Name the edge by the data it carries, not the order it happens in. Do not think “and then the verdict.” Think “styleFindings, typeFindings, and testResults flow into the verdict.” The instant you name an edge by its data, its realness becomes obvious, because a fictional edge has no data to name.

There is one more move on the edge, and it is the one that saves the most money. The work that sits between several nodes and the node that consumes them, the flatten, the dedupe, the filter, the sort, is plain code. No agent. Zero tokens. You do not spend a model call to concatenate two lists or drop the duplicates. Merging three findings lists into one before the verdict reads them is three lines of JavaScript, not a fourth trip to the model. Save the agents for judgment, which is the thing only they can do, and let ordinary functions do the plumbing, which they do for free and get right every time.

This is where the two objects start paying off together. A node with a validated output makes its outgoing edge a clean, named data channel. A clean data channel means the reduce step between edges can be plain code, because plain code can only operate on data it can trust. Sloppy nodes force you to spend model tokens re-parsing their prose, which is expensive glue standing in for cheap glue. Tight nodes make the whole graph cheaper to run and easier to read. The contracts are not bureaucracy. They are what turns a pile of calls into a machine.

A prompter asks a question. An architect draws a graph.

Two people can look at the same model and see two different machines. The prompter sees a very smart thing to converse with, and gets better results by asking better questions in a longer sequence, one after another, forever in a line. The architect sees nodes and edges, and gets better results by drawing a better graph: widening it where the work is independent, tightening the contracts where trust matters, spending tokens only where judgment lives.

The architect wins, and the win compounds, because a graph scales in a direction a conversation cannot. You do not make a line smarter by adding steps to it. You make the work faster and sturdier by seeing its true shape and drawing that instead. And the skill that unlocks all of it is the humble one you learned here: the arrow test. Point at every arrow, name the variable that crosses it, and delete the ones that carry nothing but the order you typed.

So that is the shape. The work is a graph, not a line, and you can already redraw a chain into one. But calling it a graph is only the first move, and it undersells what you are actually holding. A graph with nodes that hold state and edges that fire on conditions is not just a diagram. It is a formalism older than this entire industry, one that keeps getting rediscovered and renamed. Part 2, Every Graph Is a State Machine, shows you what a graph really is, and why naming it correctly is what makes agents reliable instead of merely fast.

AIAgentsOrchestrationArchitecture
Share:
Anshad Ameenza
About the Author

Anshad Ameenza

Lifelong Learner, Engineer, Technology Leader & Innovation Architect

20+ years of experience in technology leadership, innovation, and digital transformation. Building and scaling technology ventures.

Only if you find it useful

No pitch here. If these pieces are worth your time, you can get new ones in your inbox. If not, skip it with a clear conscience, nothing is being sold. Rare emails, no spam, leave whenever you like.

Continue Reading

Related Articles