Home / Articles / Blog

Graph Engineering for AI Agents: Multi-Agent Systems, Loops and Workflows Explained (2026)
AIAI agentsMulti-agentWorkflowsLangGraphAgent orchestration

Graph Engineering for AI Agents: Multi-Agent Systems, Loops and Workflows Explained (2026)

What graph engineering is, how loops and multi-agent topologies fit into it, the honest debate on whether multi-agent is worth it, the production layer (checkpoints, human approval, budgets), and a small tested JavaScript engine with a supervisor and approval step.

AIShrey Vijayvargiya

First we wrote better prompts. Then we learned to manage what goes into the context window. Then we built harnesses around the model, then loops that let it act and observe. Every one of those steps made a single agent better.

The next step is about the system, not the agent. In August 2026 a group of 35 researchers published Graph Engineering in the Era of LLM Agents. They argue that prompt, context, harness and loop engineering all improve one agent, but hard, long-running tasks need several specialised parts working together. Their name for designing that coordination is graph engineering.

In the last post, AI Agent Workflows with Graphs, we built the five classic workflow patterns with a 75-line engine. This post zooms out. It covers what graph engineering actually is, how loops and multi-agent systems fit into it, the honest debate about whether multi-agent is worth it, and the production pieces (checkpoints, human approval, budgets) that separate a demo from a system. The code at the end adds those pieces to a small engine, and every path in it was run.

What is graph engineering?

Graph engineering is designing an AI system as an explicit graph of steps, agents and state, then engineering how that graph behaves. That covers which step can follow which, where outputs get checked, how failures recover, where humans step in, and what each path costs.

The paper defines it as building explicit, dynamic, evolving graphs of tasks, agents and system states. Analytics Vidhya's explainer puts it more practically: your app is an executable graph of agents, tools, functions, policies, evaluators and human decisions.

Two things it is not:

  • Not GraphRAG or knowledge graphs. Those are graphs of data. Graph engineering is about graphs of execution.
  • Not a specific framework. LangGraph made the idea popular, but the same design works in Mastra, Microsoft Agent Framework, Google ADK, or 60 lines of your own JavaScript.

The ladder: from prompts to graphs

Each step up the ladder fixes the previous step's biggest failure.

LevelWhat you engineerTypical failure it fixes
Prompt engineeringThe words of one requestThe model misunderstands the task
Context engineeringWhat goes into the context window, and whenRight model, wrong or missing information
Harness engineeringThe code around the model: tools, permissions, retries, validationThe model can't act, or acts unsafely
Loop engineeringThe think → act → observe cycle and when it stopsOne shot isn't enough; the agent needs to iterate
Graph engineeringHow many loops, agents, tools and humans connect into one systemOne agent can't hold the whole task, verify itself, or survive a restart

The levels stack. A good graph still needs good prompts, clean context and a solid harness at every node. If those are new to you, start with what an agent harness is.

The building blocks, one level deeper

The workflow post covered nodes, edges and state. Graph engineering adds more kinds of each.

Node types

  • LLM node: one model call with a narrow job.
  • Tool node: search, scrape, query a database, call an API.
  • Deterministic node: plain code. Business rules belong here, not in a prompt.
  • Agent node: a whole loop that runs until it finishes or hits its budget.
  • Subgraph node: an entire graph used as one step. This is how multi-agent systems stay readable.
  • Human node: pause, ask a person, resume with their answer.

Edge types

  • Direct: always go to the next step.
  • Conditional: pick the next step from state (routing).
  • Parallel: fan out, then fan in with a reducer.
  • Loop: point back to an earlier node, always with a cap.
  • Error: where to go when a node fails.
  • Interrupt: stop here and wait for a human or an event.

State, reducers and checkpoints

State is one typed object every node reads and patches. Reducers decide how parallel patches merge. Checkpoints are snapshots of state saved after each step, so a run can resume after a crash, wait days for a human, or be replayed to debug. LangGraph's persistence docs describe the same idea: checkpointers save a thread's graph state so runs can continue, pause for humans, and recover from failures.

Loops: the smallest interesting graph

An agent is a loop with a decision node. The model looks at the task and what it knows so far, then either picks a tool or declares it's done. The tool runs, the result goes into state, and the loop repeats.

text
think ──(tool?)──▶ act ──▶ think ──(done?)──▶ finish

Loop engineering is mostly about stopping well:

  • A hard budget: a maximum number of tool calls and a maximum number of total steps, owned by your code, not the model.
  • A real exit condition: "the model said done" is one exit. "The budget ran out" must be another, with an honest message.
  • Progress checks: if two rounds add nothing new to state, stop. An agent repeating the same search is spending money, not thinking.
  • Validated actions: if the model names a tool that doesn't exist, exit or correct it. Don't crash and don't guess.

Get loops right and multi-agent systems become composition: loops inside nodes, inside graphs.

Multi-agent topologies

Once one agent loop works, you can connect several. There are only a handful of shapes worth knowing.

TopologyHow it worksGood forWatch out for
Single agent + toolsOne loop, many toolsMost tasks, honestlyContext fills up on long tasks
PipelineAgent A's output feeds agent BFixed stages (research → write → edit)Errors flow downstream
Supervisor / orchestrator-workersA lead agent splits the task and delegates to workersBroad research, many independent sourcesWorkers duplicating work; synthesis quality
Handoff (swarm)The active agent passes control and the conversation to a specialistSupport and triage flowsLosing track of who owns the user
HierarchicalSupervisors of supervisors, each team a subgraphLarge systems with clear sub-domainsLatency and debugging depth
Debate / group chatAgents critique each other until a judge decidesReviews, high-stakes decisionsCost; agents agreeing too quickly

Frameworks name these differently. The OpenAI Agents SDK uses handoffs and agents-as-tools. Microsoft Agent Framework lists sequential, concurrent, handoff and group-collaboration workflows. Google ADK ships sequential, parallel and loop agents. Underneath, they're all graphs.

Should you build multi-agent at all? The honest debate

Two of the most-cited posts on this topic disagree, and both are right.

For: In How we built our multi-agent research system, Anthropic describes a lead agent that spawns parallel subagents to search. On their internal research eval it beat a single agent by 90.2%. It also used about 15× the tokens of a normal chat (single agents use about 4×). Anthropic says it fits high-value tasks with lots of parallel work, information that doesn't fit in one context window, and many tools. It's a poor fit for tasks with tight dependencies, which includes most coding.

Against: In Don't Build Multi-Agents (June 2025), Cognition's Walden Yan argues that parallel subagents make conflicting hidden decisions. One builds a Mario-style background while another builds a bird that doesn't match. Their principles are to share full context and to remember that every action carries implicit decisions. Their default is a single-threaded agent, plus a model that compresses history for very long tasks.

How to reconcile them: parallelise reading, not writing. Subagents that gather information independently (search, read, summarise) rarely conflict, and the lead agent can reconcile what they find. Subagents that each produce part of one artefact (code, a design, an essay) make clashing choices. So:

  • Use a supervisor with parallel workers for research, data gathering and evaluation.
  • Use one agent, or a strict pipeline, when the output is a single coherent thing.
  • Always ask whether the task is worth 15× the tokens. For most product features it isn't.

The production layer: what makes a graph a system

A graph that only works when nothing goes wrong is a demo. Five pieces turn it into a system.

  1. Durable execution. Save state after every step. A crash, deploy or timeout resumes from the last checkpoint instead of starting again and paying for every LLM call twice. Anthropic's post calls out resuming from checkpoints, rather than restarting, as essential for long-running agents.
  2. Human-in-the-loop interrupts. Anything irreversible (publishing, sending, paying, deleting) goes through a human node that pauses the graph, saves state, and resumes with the person's decision, minutes or days later.
  3. Subgraphs as contracts. Each specialist agent is its own graph with a clear input and output. The supervisor sees one node. You can test, swap or upgrade an agent without touching the rest.
  4. Budgets on every loop and fan-out. Steps, tool calls, parallel width and spend, all capped in code. Multi-agent systems multiply cost by design, so cap them by design too.
  5. Traces and outcome evals. Record every node with its timing and inputs. Judge outcomes, not paths: Anthropic recommends small eval sets (about 20 queries) early on and LLM judges with a rubric. Logging every step also gives you the audit trail covered in AI agent auditing and compliance.

Build it: checkpoints, an agent loop, a supervisor and a human approval

Here's the engine from the workflow post, grown up a little. It saves a checkpoint to disk after every step, and any node can return interrupt(question) to pause the run. Resuming loads the checkpoint, merges in the human's answer, and continues from the paused node's edge.

js
// graph.mjs: a small graph engine with checkpoints and human-in-the-loop interrupts.
import { mkdir, readFile, writeFile } from "node:fs/promises";

const DIR = "./checkpoints";
const save = async (id, cp) => { await mkdir(DIR, { recursive: true }); await writeFile(`${DIR}/${id}.json`, JSON.stringify(cp, null, 2)); };
const load = async (id) => JSON.parse(await readFile(`${DIR}/${id}.json`, "utf8"));

export const interrupt = (question) => ({ __interrupt: question });

export async function run(graph, { threadId, state = {}, resume } = {}) {
  let cp = resume ? await load(threadId) : { current: graph.start, state, steps: 0, trace: [] };
  if (resume) {
    Object.assign(cp.state, resume);              // human answer goes into state
    cp.current = graph.edges[cp.pausedAt](cp.state); // continue from the paused node's edge
    delete cp.pausedAt; delete cp.question;
  }
  while (cp.current) {
    if (++cp.steps > (graph.maxSteps ?? 30)) throw new Error(`step budget exceeded at ${cp.current}`);
    const name = cp.current;
    const t = Date.now();
    const patch = (await graph.nodes[name](cp.state)) || {};
    cp.trace.push({ node: name, ms: Date.now() - t });
    if (patch.__interrupt) {                       // pause: persist and hand control to a human
      Object.assign(cp, { pausedAt: name, question: patch.__interrupt, current: null });
      await save(threadId, cp);
      return { status: "paused", question: patch.__interrupt, state: cp.state };
    }
    for (const [k, v] of Object.entries(patch)) {
      const r = graph.reducers?.[k];
      cp.state[k] = r ? r(cp.state[k], v) : v;
    }
    const edge = graph.edges[name];
    cp.current = typeof edge === "function" ? edge(cp.state) : edge ?? null;
    await save(threadId, cp);                      // checkpoint after every step
  }
  return { status: "done", state: cp.state, trace: cp.trace };
}

Now the agents. researcher is a loop with a tool budget. supervisor routes the task to a specialist, runs the researcher as a subgraph, and asks a human before anything is published. It uses OpenRouter for the model, so any model works.

js
// agents.mjs: an agent loop, a supervisor, and a human approval step.
import { run, interrupt } from "./graph.mjs";

export async function llm(prompt, { json = false, model = process.env.MODEL || "openai/gpt-4o-mini" } = {}) {
  const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ model, messages: [{ role: "user", content: prompt }], ...(json ? { response_format: { type: "json_object" } } : {}) }),
  });
  if (!res.ok) throw new Error(`OpenRouter ${res.status}`);
  const text = (await res.json()).choices[0].message.content;
  return json ? JSON.parse(text) : text;
}

// 1. The agent loop: think → act → observe, with a hard tool budget.
const TOOLS = {
  search: async (q) => `results for "${q}"`,            // swap in a real search API
  fetchPage: async (url) => `text of ${url}`,           // swap in your scraper
};

export const researcher = (ask = llm) => ({
  start: "think",
  maxSteps: 20,
  nodes: {
    think: async (s) => {
      const d = await ask(
        `Task: ${s.task}\nNotes so far: ${JSON.stringify(s.notes ?? [])}\n` +
        `Tools: ${Object.keys(TOOLS).join(", ")}. Reply JSON {"tool":"name","input":"..."} or {"done":"final answer"}.`,
        { json: true },
      );
      return { decision: d, toolCalls: (s.toolCalls ?? 0) + (d.tool ? 1 : 0) };
    },
    act: async (s) => ({ notes: [`${s.decision.tool}: ${await TOOLS[s.decision.tool](s.decision.input)}`] }),
    finish: async (s) => ({ answer: s.decision.done ?? "Stopped: tool budget used up." }),
  },
  edges: {
    think: (s) => (s.decision.done || s.toolCalls > 5 || !TOOLS[s.decision.tool] ? "finish" : "act"),
    act: "think",
    finish: null,
  },
  reducers: { notes: (old = [], add) => [...old, ...add] },
});

// 2. The supervisor: routes to specialist agents (each is its own graph), then a human approves.
export const supervisor = (ask = llm) => ({
  start: "route",
  nodes: {
    route: async (s) => {
      const { agent } = await ask(`Pick "research" or "write" for: ${s.task}. JSON {"agent":"..."}`, { json: true });
      return { agent: agent === "write" ? "write" : "research" };   // never trust the label blindly
    },
    research: async (s) => {
      const sub = await run(researcher(ask), { threadId: `${s.threadId}-research`, state: { task: s.task } });
      return { draft: sub.state.answer };                              // a subgraph is just a node
    },
    write: async (s) => ({ draft: await ask(`Write a short answer: ${s.task}`) }),
    approve: async (s) => (s.approved === undefined ? interrupt(`Publish this?\n${s.draft}`) : {}),
    publish: async (s) => ({ published: true }),
    discard: async () => ({ published: false }),
  },
  edges: {
    route: (s) => s.agent,
    research: "approve",
    write: "approve",
    approve: (s) => (s.approved ? "publish" : "discard"),
    publish: null,
    discard: null,
  },
});

Run it, stop at the approval step, then resume with the decision:

js
import { run } from "./graph.mjs";
import { supervisor } from "./agents.mjs";

const g = supervisor();
const first = await run(g, { threadId: "t1", state: { task: "Explain graph engineering", threadId: "t1" } });
console.log(first.status, first.question);   // "paused", "Publish this? ..."

// ...an hour, or a day, later, after a human clicks Approve:
const done = await run(g, { threadId: "t1", resume: { approved: true } });
console.log(done.status, done.state.published);   // "done", true

What I checked (with a fake LLM, offline):

  • Pause and resume: the run stops at approve, writes checkpoints/t1.json, and finishes with route > research > approve > publish once resumed.
  • Reject path: resuming with approved: false goes to discard.
  • Runaway loop: a model that never says "done" is stopped by the tool budget after 6 calls, with "Stopped: tool budget used up" instead of a crash.
  • Subgraph checkpoints: the researcher saves its own t1-research.json, so you can inspect exactly what the sub-agent did.

That's a real limitation worth knowing, too: checkpoints are local JSON files, so this engine is single-process. For many workers, put the same save/load behind Postgres or Redis. The graph code doesn't change.

Frameworks, if you'd rather not roll your own

All of these are open source and were updated this week.

FrameworkLanguageLicenseGraph primitiveNotes
LangGraph (JS)Python, TSMITState graph with checkpointers and interruptsThe most control over state, routing and persistence
Microsoft Agent FrameworkPython, .NETMITGraph-based workflowsSequential, concurrent, handoff, group patterns; checkpointing and time-travel
OpenAI Agents SDK (JS)Python, TSMITHandoffs, agents-as-toolsLight; guardrails and sessions built in
Google ADKPythonApache-2.0Sequential, parallel, loop agentsWorkflow agents as composable building blocks
MastraTypeScriptApache-2.0 (ee/ folders separate).then(), .branch(), .parallel() workflowsSuspend/resume for human approval; good fit for Next.js apps
CrewAIPythonMITCrews + event-driven FlowsCrews for role-based agents, Flows for precise control
Pydantic AIPythonMITpydantic_graphTyped graphs for Python teams
LlamaIndexPython, TSMITEvent-driven workflowsStrong when the graph is mostly retrieval

If you're choosing a Python SDK specifically, the Python AI agent SDK comparison goes deeper.

A graph engineering checklist

Before you ship an agent graph, check that you can answer yes to each of these:

  • Could this be one LLM call or one agent? If yes, do that.
  • Is every loop capped by steps, tool calls and spend, in code?
  • Are business rules in deterministic nodes, not prompts?
  • Does every model-chosen label, URL or tool get validated before use?
  • Do parallel branches only read, with a single owner for the final output?
  • Is state checkpointed so a crash resumes instead of restarting?
  • Does every irreversible action pass a human node?
  • Is every specialist agent a subgraph with a clear input and output?
  • Do you trace every node (name, timing, cost)?
  • Do you have about 20 eval cases that judge outcomes, not paths?

Conclusion

Prompt, context, harness and loop engineering made agents smarter. Graph engineering makes systems of agents legible: you can see every step, cap every loop, pause before every risky action, and resume after every failure. Most of it isn't new computer science. It's workflow engineering, applied to components that happen to be non-deterministic.

Start with one loop. Add a supervisor only when the work genuinely splits into independent reading. Put a human in front of anything you can't undo. And keep the graph small enough that you can still draw it on one page.

The research agent that reads 100 blogs a day is a real graph built on these ideas. More builder guides like this go out in the monthly ihatereading magazine.

External Links (15)

Explore topics

More in

Weekly letter

Subscribe to iHateReading

Our once-a-week newsletter on programming, jobs, AI, and building products online.

Our once a week newsletter on Programming, Jobs, AI, and Business