Adr
ADR-006: Workflow DAG Design
Directed acyclic graph for multi-agent orchestration
ADR-006: Workflow DAG Design
Status
Accepted
Context
Agent Studio orchestrates multi-agent voice workflows where:
- Multiple agents handle different parts of a conversation
- Agents can "hand off" to other agents
- Some agents can be skipped based on conditions
- The workflow has a defined entry and exit point
We need a data structure that:
- Represents agent sequences and branches
- Supports conditional routing
- Is easy to visualize in a UI
- Can be validated for correctness
Options Considered
- Linear sequence - Simple list of agents
- State machine - States and transitions
- DAG (Directed Acyclic Graph) - Nodes and connections
- BPMN-style workflow - Complex but powerful
Decision
We will use a DAG with nodes and connections:
{
"nodes": [
{
"id": "node-greeter",
"agent_name": "greeter-agent",
"is_entry": true
},
{
"id": "node-meal",
"agent_name": "meal-agent",
"skip_condition": "flags.skip_meal"
},
{
"id": "node-glucose",
"agent_name": "glucose-agent",
"skip_condition": "flags.skip_glucose"
},
{
"id": "node-feedback",
"agent_name": "feedback-agent",
"is_exit": true
}
],
"connections": [
{
"source_id": "node-greeter",
"target_id": "node-meal",
"context_passed": ["user_name", "user_state"]
},
{
"source_id": "node-meal",
"target_id": "node-glucose"
},
{
"source_id": "node-glucose",
"target_id": "node-feedback"
}
]
}Visual Representation
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Greeter │────▶│ Meal │────▶│ Glucose │────▶│ Feedback │
│ (entry) │ │ (skippable) │ │ (skippable) │ │ (exit) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘Node Properties
| Property | Type | Description |
|---|---|---|
id | string | Unique identifier |
agent_name | string | Reference to agent |
is_entry | boolean | Starting point (exactly one) |
is_exit | boolean | Ending point (at least one) |
skip_condition | string | Context path to evaluate |
Connection Properties
| Property | Type | Description |
|---|---|---|
source_id | string | From node |
target_id | string | To node |
context_passed | string[] | Context keys to pass |
condition | string | Optional routing condition |
Workflow Runner Logic
class WorkflowRunner:
async def get_next_node(self, current_node_id: str) -> str | None:
"""Determine next node based on connections and conditions."""
connections = self.get_connections_from(current_node_id)
for conn in connections:
# Check connection condition
if conn.condition and not self.evaluate(conn.condition):
continue
target_node = self.get_node(conn.target_id)
# Check skip condition
if target_node.skip_condition:
if self.evaluate(target_node.skip_condition):
# Recursively find next non-skipped node
return await self.get_next_node(target_node.id)
return target_node.id
return None # End of workflowValidation Rules
- Exactly one node with
is_entry: true - At least one node with
is_exit: true - All nodes reachable from entry
- No cycles (acyclic)
- All
agent_namereferences exist
def validate_workflow(config: WorkflowConfig) -> list[str]:
errors = []
# Check entry/exit nodes
entries = [n for n in config.nodes if n.is_entry]
exits = [n for n in config.nodes if n.is_exit]
if len(entries) != 1:
errors.append("Exactly one entry node required")
if len(exits) < 1:
errors.append("At least one exit node required")
# Check for cycles
if has_cycle(config.nodes, config.connections):
errors.append("Workflow contains cycles")
# Check reachability
unreachable = find_unreachable_nodes(config)
if unreachable:
errors.append(f"Unreachable nodes: {unreachable}")
return errorsConsequences
Positive
- Clear visual representation
- Easy to build UI editor
- Supports conditional routing
- Validates at save time
- Handles skipped agents gracefully
Negative
- No parallel execution (sequential only)
- Limited branching (no merge/join)
- Skip conditions evaluated at runtime
Future Extensions
- Parallel branches with join nodes
- Sub-workflows (workflow as a node)
- Loop constructs (with max iterations)