Skip to main content
AI Agent DAG: Run Only What Can Run in Parallel

AI Agent DAG: Run Only What Can Run in Parallel

AI Integration
••6 min read•By Daily Miranda Pardo

You have three tasks your agent needs to run. Two of them can start immediately because they depend on nothing. The third needs the result of both. How do you implement this?

The usual answer is a sequential chain: A, then B, then C. It works. But if A and B each take 800ms, you're wasting 800ms of wall-clock time you could have used running them in parallel. In an agent that handles dozens of requests per day, that waste stacks up fast.

The other extreme — throwing everything into Promise.all — doesn't work if C depends on the output of A and B. You either get runtime errors or, worse, an agent mixing incomplete context and returning incoherent responses.

The solution is a DAG.

What a DAG is and why it matters for AI agents

DAG stands for Directed Acyclic Graph — a directed graph with no cycles. In agent orchestration, each node is a task and each edge means one task depends on the result of another.

The key insight is that a DAG automatically resolves execution order by grouping tasks into waves. In the first wave you run everything with no dependencies, in parallel. When those complete, you run the second wave — tasks that depend only on the first. And so on until you reach the final node.

The result: maximum parallelism without breaking dependencies.

This pattern comes up repeatedly in the AI agent integration projects we build at DAILYMP. As soon as a workflow has more than three steps with different dependencies, a sequential chain leaves both time and money on the table.

The problem with sequential chains

Imagine an agent processing a customer support request. It needs to:

  1. Extract entities from the message (name, contract number, issue)
  2. Classify intent and urgency
  3. Fetch the customer's history from the database (needs the name/contract from step 1)
  4. Determine which specialist agent to route to (needs the classification from step 2)
  5. Generate the final response (needs both the history from step 3 and routing from step 4)

Sequential: 5 × ~800ms = 4 seconds.

With a DAG: steps 1 and 2 run in parallel (800ms). When they finish, steps 3 and 4 run in parallel (800ms). When they finish, step 5 runs (800ms). Total: 2.4 seconds — 40% less latency without changing a single model or optimizing a single prompt.

TypeScript implementation

The core structure is straightforward. Each node has an identifier, an executor function, and an array of dependencies:

type NodeId = string;

interface DAGNode<T = unknown> {
  id: NodeId;
  deps: NodeId[];
  run: (context: Map<NodeId, unknown>) => Promise<T>;
}

interface DAGResult {
  outputs: Map<NodeId, unknown>;
  durationMs: number;
}

The context is a Map where each node writes its result when it finishes. Nodes in the next wave read from it to access the data they need.

The wave executor

async function runDAG(nodes: DAGNode[]): Promise<DAGResult> {
  const outputs = new Map<NodeId, unknown>();
  const completed = new Set<NodeId>();
  const start = Date.now();

  while (completed.size < nodes.length) {
    // Current wave: nodes whose deps are all done
    const wave = nodes.filter(
      (n) =>
        !completed.has(n.id) &&
        n.deps.every((dep) => completed.has(dep))
    );

    if (wave.length === 0) {
      throw new Error("DAG cycle detected — check your dependencies");
    }

    await Promise.all(
      wave.map(async (node) => {
        const result = await node.run(outputs);
        outputs.set(node.id, result);
        completed.add(node.id);
      })
    );
  }

  return { outputs, durationMs: Date.now() - start };
}

Thirty lines. No external libraries. That's the entire engine.

Real usage: the support agent

const pipeline: DAGNode[] = [
  {
    id: "extract",
    deps: [],
    run: async () => extractEntities(userMessage),
  },
  {
    id: "classify",
    deps: [],
    run: async () => classifyIntent(userMessage),
  },
  {
    id: "history",
    deps: ["extract"],
    run: async (ctx) => {
      const { customerId } = ctx.get("extract") as ExtractResult;
      return fetchCustomerHistory(customerId);
    },
  },
  {
    id: "route",
    deps: ["classify"],
    run: async (ctx) => {
      const { intent, urgency } = ctx.get("classify") as ClassifyResult;
      return resolveTargetAgent(intent, urgency);
    },
  },
  {
    id: "respond",
    deps: ["history", "route"],
    run: async (ctx) => {
      const history = ctx.get("history") as CustomerHistory;
      const agent = ctx.get("route") as AgentRoute;
      return generateResponse(userMessage, history, agent);
    },
  },
];

const { outputs, durationMs } = await runDAG(pipeline);
const response = outputs.get("respond") as string;

The code declares dependencies declaratively. The executor resolves the order. If you add a new node tomorrow that depends on nothing, it runs in parallel with extract and classify without touching anything else.

Partial vs. total failure handling

By default, if a node throws, Promise.all rejects the whole wave. Sometimes that's what you want. But sometimes a node failing shouldn't block independent nodes.

Capture errors per node and decide:

await Promise.all(
  wave.map(async (node) => {
    try {
      const result = await node.run(outputs);
      outputs.set(node.id, result);
      completed.add(node.id);
    } catch (err) {
      if (node.optional) {
        outputs.set(node.id, null);
        completed.add(node.id);
      } else {
        throw err; // Propagates and cancels the DAG
      }
    }
  })
);

Add optional: true to nodes that can fail without breaking the final output. Critical nodes propagate errors and halt execution. You get granular fault tolerance without building a custom retry system.

When to use a DAG and when not to

A DAG pays its conceptual cost when:

  • You have 3 or more tasks with partial cross-dependencies
  • Individual tasks have measurable latency (LLM calls, database queries, external APIs)
  • The workflow will grow — more nodes in the future are inevitable

Skip it when:

  • You have 2 tasks that always run in order — a simple chain is more readable
  • Every task depends on the previous one — that's a chain, not a graph
  • Each task takes < 50ms — the coordination overhead isn't worth it

The mistake most teams make

Trying to parallelize without modelling dependencies. They reach for Promise.all over all tasks and then, when one node needs another's output, they resolve it with flags, shared variables, or manually reordering the array. The result is brittle code nobody understands three months later.

A DAG makes that contract explicit. Dependencies are data, not hidden control flow. When a new team member reads the node array, they see exactly what depends on what without tracing execution manually.

In the AI Driven Development projects we build at DAILYMP, this pattern is one of the first things we install in any agent with more than three steps. The latency win is immediate. The maintainability win is larger still.


Building an agent with chained steps and finding it slower than it should be? Tell me what your pipeline looks like on WhatsApp — I'll show you how to model it as a DAG and how much latency you can recover.

Let's talk on WhatsApp →

Share article

Repetitive processes in your business?

Download the free AI Automation Map — the 5 most time-consuming processes and how to fix them.

No spam. Just the PDF. Unsubscribe anytime.

Written by Daily Miranda Pardo

I help businesses automate processes, build AI agents and connect intelligent systems.