Agent Studio
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

  1. Linear sequence - Simple list of agents
  2. State machine - States and transitions
  3. DAG (Directed Acyclic Graph) - Nodes and connections
  4. 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

PropertyTypeDescription
idstringUnique identifier
agent_namestringReference to agent
is_entrybooleanStarting point (exactly one)
is_exitbooleanEnding point (at least one)
skip_conditionstringContext path to evaluate

Connection Properties

PropertyTypeDescription
source_idstringFrom node
target_idstringTo node
context_passedstring[]Context keys to pass
conditionstringOptional 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 workflow

Validation Rules

  1. Exactly one node with is_entry: true
  2. At least one node with is_exit: true
  3. All nodes reachable from entry
  4. No cycles (acyclic)
  5. All agent_name references 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 errors

Consequences

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)

On this page