Skip to main content
Execution graphs let you orchestrate multi-agent workflows as directed acyclic graphs (DAGs). Each node in the graph is a task executed by an agent, with dependencies determining execution order. Nodes without dependencies run in parallel. The graph coordinator manages scheduling, result forwarding between nodes, timeout enforcement, and aggregate completion.

Domain Types

The execution graph system is built on three core types defined in @comis/core. These types represent the graph structure, individual nodes, and resource limits.

ExecutionGraph

GraphNode

Each node represents a sub-agent task with optional dependency constraints, per-node timeouts, retry behavior, built-in node types, and context verbosity control. Upstream node outputs are referenced directly via {{nodeId.result}} templates in task text, where nodeId must appear in the node’s dependsOn array.

GraphBudget

Resource limits applied across the entire graph execution.
When a budget limit is exceeded during execution, the coordinator cancels all running nodes and fails the graph.

Node Lifecycle

Each node transitions through a defined set of states during execution:
  • pending — Waiting for dependencies to reach terminal state
  • ready — All dependency barriers satisfied, eligible for scheduling
  • running — Agent is actively executing the task
  • completed — Task finished successfully with output
  • failed — Task encountered an error
  • skipped — Dependencies failed and barrier cannot be satisfied
Graph-level states: running -> completed | failed | cancelled

Failure Strategies

The onFailure field controls how the graph responds when a node fails:
  • fail-fast (default) — If any node fails, skip all dependent nodes (cascade) and fail the graph as soon as all running nodes finish. Dependents with failed or skipped dependencies are immediately marked as skipped.
  • continue — If a node fails, only skip dependent nodes whose barrier can never be satisfied. Independent nodes and nodes with satisfied barriers continue executing.

Barrier Modes

For nodes with multiple dependencies, the barrierMode controls when the node becomes ready: All barrier modes require dependencies to reach a terminal state (completed, failed, or skipped) before the node becomes ready. A best-effort node does not fire as soon as one dependency completes — it waits for all dependencies to finish.

Graph Validation

The validateAndSortGraph() function performs both schema and structural validation before a graph can execute.

Schema Validation (Zod)

  • Node count: 1-20 nodes
  • Required fields: nodeId (non-empty string), task (non-empty string)
  • Type checks on all optional fields

Structural Validation (DAG)

  • Unique node IDs — No two nodes can share the same nodeId
  • No self-dependencies — A node cannot list itself in dependsOn
  • Valid references — All dependsOn entries must point to existing nodes
  • Acyclicity — The graph must be a DAG (no circular dependencies)
Cycle detection uses Kahn’s algorithm for topological sorting. When a cycle is detected, a DFS trace identifies the exact cycle path for actionable error messages.

GraphValidationError

Validation failures return a structured GraphValidationError with a kind field:

Usage

The returned ValidatedGraph contains the original graph paired with its topological execution order — the sequence in which nodes can be safely scheduled.

Graph Coordinator

The graph coordinator is the runtime engine that executes validated graphs end-to-end. It lives in the daemon process and orchestrates node scheduling, result forwarding, and lifecycle management. Key responsibilities:
  • Node spawning — Each node is spawned as a sub-agent via the SubAgentRunner. Nodes without dependencies start immediately; dependent nodes wait for their barriers.
  • Completion tracking — Event-driven via session:sub_agent_completed events. A single global handler routes completion events to the correct graph instance.
  • Result forwarding — Upstream node outputs are injected into downstream node task descriptions via {{nodeId.result}} template interpolation (see Data Flow).
  • Timeout enforcement — Both per-node timeouts (kills the individual sub-agent) and graph-level timeouts (cancels all running nodes).
  • Concurrency limiting — Three-tier concurrency control: per-node (maxParallelSpawns, default 10 for spawn_all typed nodes), per-graph (maxConcurrency, default 4 concurrent nodes), and global (maxGlobalSubAgents, default 20 across all graphs). Ready nodes wait in a FIFO queue when limits are reached.
  • Aggregate announcement — A single completion message summarizing all node results is sent to the originating channel when the graph finishes.
The graph coordinator runs inside the daemon process. It is not directly accessible from other packages — you interact with execution graphs by creating them through the CLI, RPC, or web dashboard.

Graph Events

The event bus emits events at key points during graph execution: Subscribe to these events via the Event Bus for monitoring, logging, or triggering downstream actions.

Data Flow

Nodes pass data to downstream nodes using {{nodeId.result}} templates directly in the task text. The nodeId in the template must appear in the consuming node’s dependsOn array.
When node research completes, its output text replaces {{research.result}} in the summarize node’s task description. This creates a direct data flow between nodes without shared state or intermediate variable mappings.

Context Envelope

The coordinator also builds a context envelope around each node’s task, providing awareness of the graph structure and upstream results. The contextMode field controls how much upstream output is included in the envelope (see Context Verbosity Modes).

User Variables

Use ${VARIABLE_NAME} syntax in task text for values the user provides at execution time. Variables are resolved before template interpolation when the graph starts via the variables parameter passed to graph.execute.
User variables are validated at define time — graph.define returns a userVariables array listing all ${VAR} placeholders found in node tasks. Unresolved variables at execution time produce a warning but do not block execution.
User variable values are escaped to prevent injection of {{nodeId.result}} templates. A zero-width space character is inserted into any {{ or }} sequences in variable values.

Shared Data Folder

When a graph starts, the coordinator creates a per-graph shared directory at ~/.comis/graph-runs/{graphId}/. All nodes in the graph get read-write access to this directory, enabling file-based data exchange between nodes. Each node receives the shared folder path in its context envelope under a “Shared Pipeline Folder” section. Nodes can write output files — reports, structured data, artifacts — for other nodes to read. This complements template-based data passing with file-based exchange for larger payloads that would exceed the result truncation limit.

Directory structure

Lifecycle

The shared directory is created with owner-only permissions (0o700) at graph start. The directory persists after graph completion so output files remain accessible. Driver artifacts (debate transcripts, vote tallies, etc.) are also written to this directory (see Node Types).

Security

The directory is created with 0o700 permissions, restricting access to the owner. File tool access is enforced through the safe-path-wrapper’s sharedPaths mechanism, which grants read-write access only to nodes within the same graph execution. Nodes in different graph runs cannot access each other’s shared directories.
The shared data folder is separate from {{nodeId.result}} template passing. Use templates for passing text results between nodes. Use the shared folder for larger artifacts like files, reports, or structured data that would exceed the result truncation limit (default 12000 characters).

Per-Node Retry

Nodes can be configured to retry automatically on failure using the retries field. When a node fails and has retries remaining, the coordinator waits with exponential backoff (1s, 2s, 4s) before restarting the node from scratch. The node’s status transitions back to ready during retry, making it visually distinct from permanently failed nodes.
Runtime state includes retryAttempt (current attempt number, starting at 0) and retriesRemaining (how many retries are left). These are visible in graph.status responses.
Retries restart the node completely — the sub-agent begins fresh with no memory of the failed attempt. For typed nodes (debate, vote, etc.), retry restarts the entire driver from scratch (no partial continuation). Retries on approval-gate nodes will re-prompt the user.

NodeTypeDriver Interface

The NodeTypeDriver interface defines the contract for pluggable graph node type drivers. Drivers are pure synchronous functions — they receive context and return action objects that the graph coordinator interprets and executes. The coordinator handles all async operations (agent spawning, I/O, event emission).

NodeDriverAction

Every driver method returns a NodeDriverAction — a discriminated union identified by the action field. The coordinator interprets each action and performs the corresponding async operation.

NodeDriverContext

The NodeDriverContext object is passed to every driver method, providing node metadata and a state bag for persisting data across turns. The getState / setState pair enables drivers to track multi-turn progress (e.g., current debate round, accumulated vote tallies, refinement chain position) without external storage.

Driver Lifecycle

The coordinator drives execution through a turn-based loop, calling driver methods and interpreting the returned actions.
  1. Initialize — The coordinator calls initialize(ctx) on the driver, which returns the first action (typically spawn for sequential drivers or spawn_all for parallel drivers). A graph:driver_lifecycle event fires with phase initialized.
  2. Turn loop (sequential) — For sequential drivers (debate, refine, collaborate): the coordinator spawns a single agent. When the agent completes, the coordinator calls onTurnComplete(ctx, output) which returns the next action. This loop continues until the driver returns complete or fail.
  3. Turn loop (parallel) — For parallel drivers (vote, map-reduce): the coordinator spawns all agents via spawn_all. When all agents complete, the coordinator calls onParallelTurnComplete(ctx, outputs) which returns the next action (e.g., complete for vote tallying, or spawn for the reducer in map-reduce).
  4. Completion — When the driver returns complete, a graph:driver_lifecycle event fires with phase completed, the output is stored, and dependent nodes become eligible for scheduling.
  5. Failure — When the driver returns fail or an unhandled error occurs, a graph:driver_lifecycle event fires with phase failed, and the node transitions to the failed state.
  6. Abort — On timeout, cancellation, or graph shutdown, the coordinator calls onAbort(ctx) and emits graph:driver_lifecycle with phase aborted.
Each transition emits a graph:driver_lifecycle event with the corresponding phase value. See the Event Bus reference for the full event payload.

Three-Tier Concurrency Model

The coordinator enforces three independent concurrency limits to prevent resource exhaustion while maximizing parallelism.

Tier 1 — Per-Node Parallel Cap (maxParallelSpawns)

Limits how many agents a single spawn_all action can start simultaneously. Prevents a single map-reduce node with 50 mappers from monopolizing all available agent slots.

Tier 2 — Per-Graph Concurrency (maxConcurrency)

Limits how many nodes within one graph can execute concurrently. Independent nodes with satisfied dependencies run in parallel up to this cap. Additional ready nodes wait in a per-graph queue.

Tier 3 — Global Sub-Agent Cap (maxGlobalSubAgents)

Limits the total number of concurrent sub-agents across ALL active graphs. When the cap is hit, new spawns queue in FIFO order and drain as agents complete. This prevents multiple concurrent graphs from overwhelming LLM provider rate limits or system resources. The graph.status RPC (called without a graphId) returns live concurrency stats:
See the JSON-RPC reference for the full graph.status response format.
The per-graph and global caps are configurable via YAML config under security.agentToAgent.graphMaxConcurrency and security.agentToAgent.graphMaxGlobalSubAgents. The per-node parallel cap (maxParallelSpawns) is currently a coordinator default (10) and is not exposed as a YAML key.

Node Types

Nodes can optionally use a built-in node type via the typeId and typeConfig fields. When typeId is set, the node uses a driver that controls multi-agent orchestration patterns (debates, voting, review chains, etc.) instead of spawning a single sub-agent. If typeId is not set, the node behaves as a regular single-agent task — this is the default and most common pattern.

Available Types

debate — Multi-Round Adversarial Debate

Two or more agents argue in rounds, with an optional synthesizer producing the final output. In each round, agents speak in round-robin order. Each agent sees the full debate transcript from prior turns. After all rounds complete, an optional synthesizer agent produces the final output based on the entire discussion.
The debate transcript is saved to {nodeId}-debate-transcript.md in the graph’s shared data folder. If no synthesizer is specified, the last debater’s final turn becomes the node output.

vote — Parallel Independent Voting

All agents vote independently and concurrently, with results tallied into a summary.

refine — Sequential Refinement Chain

Each agent reviews and improves the previous agent’s output, like an editorial pipeline.

collaborate — Sequential Multi-Perspective

Agents contribute perspectives sequentially, each building on all prior contributions. Collaborative, not adversarial.

approval-gate — Human Approval Checkpoint

Pauses pipeline execution and sends a message to the user’s channel, waiting for their response before continuing. Requires the graph to be triggered from a channel context (Telegram, Discord, etc.).
The user responds via their chat channel. Approval keywords (yes, approve, go, confirm) continue the pipeline. Denial keywords (no, deny, stop, reject) fail the node.

map-reduce — Parallel Processing with Aggregation

Splits work across multiple agents in parallel, then a reducer agent aggregates all results.
The agentId field on a typed node is ignored — typed nodes use agents specified in their typeConfig. Setting both produces a validation warning.
Custom driver registration is not yet available in the public API. The 7 built-in drivers (agent, debate, vote, refine, collaborate, approval-gate, map-reduce) cover the standard multi-agent orchestration patterns. The NodeTypeDriver interface is documented above for contributors and internal extension.

Context Verbosity Modes

The contextMode field controls how much upstream output the coordinator includes in each node’s context envelope.
Use contextMode: "none" with explicit {{nodeId.result}} templates for maximum control over what data the node sees. Use contextMode: "summary" to reduce token usage on nodes that only need a high-level overview of upstream work.

Example: Research Pipeline

A complete execution graph that researches a topic, analyzes sources, and writes a report:
This pipeline creates a three-stage sequential workflow: gather -> analyze -> write. Each node runs with a dedicated agent, and outputs flow downstream via {{nodeId.result}} templates in task text. The fail-fast strategy ensures the pipeline stops quickly if any stage fails, and the budget limits prevent runaway costs.
inputFrom and inputMapping are not part of the graph schema — the graph.load RPC strips those fields from persisted graph definitions on load. Use the {{nodeId.result}} pattern in task text to flow outputs downstream.

Web Dashboard: Pipelines

Visual pipeline builder and monitor

Plugins

Hook into graph lifecycle events

Architecture

Domain types and validation

Event Bus

graph:* events reference