Inside Sim Studio's DAG Executor: Building a Production-Grade Workflow Engine for AI Agents
The bottom line: Sim Studio (28.7k GitHub stars, Apache 2.0) ships a DAG-based execution engine that treats workflows as compiled directed acyclic graphs — not sequential step runners [1]. A ready queue dispatches all unblocked nodes concurrently, sentinel nodes make loops acyclic, and a BlockHandler interface decouples block logic from orchestration. The architecture avoids the two most common workflow engine failure modes: artificial sequentialization of parallel branches, and monolithic block execution that conflates dispatch logic with business logic.
Why DAG Over Sequential Pipelines?
Most workflow tools model execution as a pipeline: step 1 → step 2 → step 3. This is simple but fundamentally limited when branches, conditionals, or parallel subflows are involved. Either you introduce layering (run all of branch A, then all of branch B) which serializes inherently parallel work, or you push parallelism logic into the blocks themselves, violating separation of concerns.
Sim took a different approach common in build systems and data pipelines: compile the workflow to a DAG first, then execute via topological sort [1]. Every block becomes a node, every connection becomes an edge. The topology is fully defined before the first block executes.
Workflow (visual) → Compiler → DAG (nodes + edges) → Executor
This is the same philosophy as Make, Bazel, and Airflow — but applied to AI agent workflows where blocks are LLM calls, API requests, and conditional logic rather than compilation units.
The Ready Queue: Parallelism by Default
The core of Sim’s executor is an event-driven ready queue. Instead of traversing the graph layer by layer, it maintains a set of nodes whose incoming edges are all satisfied. When a node completes, it removes its outgoing edges from downstream nodes’ dependency sets. Any node whose incoming edge count hits zero enters the queue [2].
// Simplified ready queue logic
class ExecutionEngine {
readyQueue: Set<string> = new Set();
nodeDeps: Map<string, Set<string>> = new Map(); // nodeId → incoming edge IDs
onNodeComplete(completedNodeId: string) {
const outEdges = this.getOutgoingEdges(completedNodeId);
for (const edge of outEdges) {
const target = edge.targetId;
this.nodeDeps.get(target)!.delete(edge.id);
if (this.nodeDeps.get(target)!.size === 0) {
this.readyQueue.add(target);
}
}
this.dispatchReady();
}
dispatchReady() {
for (const nodeId of this.readyQueue) {
this.readyQueue.delete(nodeId);
this.executeNode(nodeId); // parallel dispatch
}
}
}
The key property: parallelism emerges from the graph structure itself [2]. Independent blocks launch concurrently without any explicit parallel annotation. A parallel subflow with 50 branches — each branch containing multiple sequential blocks — naturally dispatches all entry nodes across all branches simultaneously.
This contrasts sharply with layer-based approaches. n8n’s v1 engine executed branches sequentially. More sophisticated engines interleave nodes across branches but still impose a global execution frame. Sim’s ready queue has no frames — each node launches as soon as its dependencies resolve, regardless of which branch or layer it belongs to.
Sentinel Nodes: Making Loops Acyclic
Loops are inherently cyclic — a workflow engine that supports them must either accept cycles (breaking the DAG property) or transform them. Sim chose transformation via sentinel nodes [1].
A loop compiles into:
- Sentinel Start — activates the first block inside the loop body
- Sentinel End — evaluates the loop condition. Returns either “continue” (triggering Sentinel Start again) or “exit” (activating blocks after the loop)
[Sentinel Start] → [Loop Body] → [Sentinel End]
↑ │
└─────────── "continue" ────────────┘
After “continue,” the executor clears execution state of blocks inside the loop, restores incoming edges, increments the iteration counter, and loads the next item. Loop variables shadow outer scopes so <loop.iteration> and <loop.item> resolve correctly [2].
On “exit,” the Sentinel End activates downstream blocks. The DAG property is preserved because the backward edge only fires on the “continue” path — at graph definition time, every path is unambiguously forward except the sentinel-mediated loopback.
The BlockHandler Pattern: Clean Dispatch
Each block type has a dedicated handler implementing a two-method interface:
interface BlockHandler {
canHandle(blockType: string): boolean;
execute(context: ExecutionContext): Promise<BlockResult>;
}
Sim ships 13+ handlers: AgentHandler, ApiHandler, ConditionHandler, EvaluatorHandler, FunctionHandler, GenericHandler, HumanInTheLoopHandler, LoopHandler, ParallelHandler, RouterHandler, TriggerHandler, VariableHandler, WaitHandler, WorkflowHandler [1].
When a node enters the ready queue, the executor iterates handlers — canHandle() checks the block type — and delegates to the first match. This separates:
- Orchestration (ready queue, edge management, state) in the executor
- Business logic (prompt construction, API calls, condition evaluation) in handlers
This is the same pattern used by Express.js middleware, Koa context handlers, and GraphQL resolvers — but applied to workflow blocks rather than HTTP requests.
Human-in-the-Loop: Pause/Resume Snapshots
Long-running AI workflows need human intervention. Sim’s executor handles this through a snapshot-based pause mechanism [1].
When a block returns a pause metadata flag:
- The executor stops processing its outgoing edges
- A full state snapshot is captured: every block output, loop iteration counter, parallel branch progress, routing decision, and remaining DAG dependency graph
- A resume URL is generated with the snapshot ID
- A notification tool delivers the URL to the authorized user
On resume, the snapshot is deserialized, the executor reconstructs all dependency sets, and processing continues from the paused block’s outgoing edge — not from scratch.
interface ExecutionSnapshot {
nodeOutputs: Map<string, BlockResult>;
loopStates: Map<string, { iteration: number; items: any[] }>;
parallelStates: Map<string, { branchIndex: number; terminalCount: number }>;
remainingDeps: Map<string, Set<string>>;
blockedNodes: Set<string>;
}
This is critical for agent workflows with approval gates, manual data review, or cost gating before expensive API calls.
Variable Resolution Hierarchy
Variables in Sim execute at two separate times: config resolution at serialization time, and parameter resolution at execution time. The resolver uses a scoping chain (inner shadows outer) [2]:
- Loop items —
<loop.iteration>,<loop.item>(most specific) - Parallel branch indices —
<parallel.index> - Workflow variables —
<workflow.variableName> - Environment variables —
${API_KEY} - Upstream block outputs —
<blockId.output.content>(least specific, most common)
The resolver is a composable chain — each scope type is a separate module (block.ts, env.ts, loop.ts, parallel.ts, reference.ts, workflow.ts) that the resolver consults in order. This makes it trivial to add new scope types without modifying existing resolvers.
Edge Management and Branch Pruning
When a condition or router block executes, only the matching outgoing edge remains active. The edge manager deactivates all other edges and cascades downstream: recursively deactivate edges from inactive branches unless a downstream node has other active incoming edges (convergence scenario) [1].
function deactivateBranch(edgeId: string) {
edge.active = false;
const target = edges.get(edgeId)!.targetId;
const outEdges = getOutgoingEdges(target);
for (const outEdge of outEdges) {
if (getIncomingEdges(outEdge.targetId).every(e => !e.active)) {
deactivateBranch(outEdge.id); // cascade
}
}
}
This practically eliminates wasted work — unreachable blocks never enter the ready queue. If two branches reconverge at a merge node, the merge block becomes ready only when all remaining active incoming edges complete. Pruned branches reduce the count, so the merge fires at the right time without manual join configuration.
Production Architecture
The executor runs server-side. The client communicates via SSE (Server-Sent Events) — block start, completion, streaming output, and errors fire as typed events [1]. The client reconstructs execution state from the event stream without polling.
Client (workflow builder) ← SSE events → Server (executor)
│
├── BlockHandler registry
├── ExecutionEngine (ready queue)
├── EdgeManager (branch pruning)
├── VariableResolver (scoped chain)
└── SnapshotService (pause/resume)
The realtime server avoids React, Next.js, the block registry, and the executor entirely — it’s built on Socket.IO with Bun runtime, dedicated to SSE delivery and nothing else [1].
Comparison With Other Approaches
n8n — Executes nodes sequentially within a layer. Parallel branches run serially. Sim’s ready queue gives true concurrency without workarounds [2].
Temporal — Durable execution with workflow-as-code. Sim is declarative (visual workflow → DAG). Temporal gives you retries and history in exchange for writing the orchestration logic yourself.
Airflow — DAG-based with a scheduler dependency. Designed for batch data pipelines (hours-long runs). Sim targets interactive AI workflows (seconds-to-minutes, streaming, human-in-the-loop).
LangGraph — Graph-based agent orchestration with typed state. More flexible for complex agent topologies but requires explicit graph construction. Sim’s visual DAG is better for non-code workflow authors.
Verdict
Sim Studio’s executor architecture demonstrates four production-grade design decisions worth borrowing:
- Ready queue over layer dispatch — parallelism is a structural property, not an annotation
- Sentinel nodes for loops — preserves DAG semantics without sacrificing loop support
- BlockHandler interface — clean separation of orchestration and business logic
- Snapshot-based pause/resume — handles human-in-the-loop without replaying the entire workflow
For teams building workflow engines, agent orchestration platforms, or any system that sequences LLM calls with conditional branching, these patterns are directly applicable. The source code is Apache 2.0 and well-modularized under executor/ and packages/workflow-types/.
References
[1] Sim Studio GitHub Repository — https://github.com/simstudioai/sim
[2] “Inside the Sim Executor” — Sim Architecture Blog — https://www.staging.sim.ai/studio/executor
[3] Sim Workflow Types — Block Definitions — https://github.com/simstudioai/sim/blob/main/packages/workflow-types/src/blocks.ts
[4] Sim Workflow Persistence — Load/Save Helpers — https://github.com/simstudioai/sim/blob/main/packages/workflow-persistence/src/index.ts
[5] Sim Workflow Authz — LockedError and getActiveWorkflowContext — https://github.com/simstudioai/sim/blob/main/packages/workflow-authz/src/index.ts
[6] Sim Real-time Protocol — SSE Event Schemas — https://github.com/simstudioai/sim/blob/main/packages/realtime-protocol/src/schemas.ts
📖 Related Reads
- NoCode Insider — AI workflow automation with no-code tools, agents, and APIs
Cross-links automatically generated from NiteAgent.
← Back to all posts