Graphs
Graphs are the foundation of Mesh. They define the structure and flow of your multi-agent workflows.
What is a Graph?
A graph in Mesh is a directed graph composed of:
- Nodes: Units of execution (agents, LLMs, tools, etc.)
- Edges: Connections between nodes defining execution flow
- Entry Point: The starting node for execution
- Controlled Cycles: Optional loops with exit conditions or iteration limits
Graph Structure
START (implicit)
↓
[entry_node]
↓
[node_a]
↓
[node_b]
↓
END (implicit)
Key Properties
- Directed: Edges have a direction (source → target)
- Controlled Cycles: Loops are allowed with proper controls (conditions or max iterations)
- Connected: All nodes must be reachable from START
Building Graphs
Programmatic API (StateGraph)
The recommended way to build graphs is using the StateGraph
builder:
from mesh import StateGraph
graph = StateGraph()
# Add nodes
graph.add_node("agent", my_agent, node_type="agent")
graph.add_node("tool", my_function, node_type="tool")
# Add edges
graph.add_edge("START", "agent")
graph.add_edge("agent", "tool")
# Set entry point
graph.set_entry_point("agent")
# Compile
compiled = graph.compile()
React Flow JSON (Declarative)
Parse Flowise-compatible JSON:
from mesh import ReactFlowParser, NodeRegistry
registry = NodeRegistry()
parser = ReactFlowParser(registry)
flow_json = {
"nodes": [
{"id": "agent_1", "type": "agentAgentflow", "data": {...}},
{"id": "llm_1", "type": "llmAgentflow", "data": {...}}
],
"edges": [
{"source": "agent_1", "target": "llm_1"}
]
}
graph = parser.parse(flow_json)
Graph Lifecycle
Build → Validate → Compile → Execute
1. Build
Add nodes and edges:
graph = StateGraph()
graph.add_node("step1", None, node_type="llm")
graph.add_node("step2", None, node_type="llm")
graph.add_edge("START", "step1")
graph.add_edge("step1", "step2")
2. Validate
Check for common issues:
graph.set_entry_point("step1")
compiled = graph.compile() # Validates automatically
Validation checks:
- Entry point is set
- No uncontrolled cycles (all cycles must have loop controls)
- All nodes are connected
- No orphaned nodes
- Loop edges have proper controls (condition or max_iterations)
3. Compile
Creates an executable ExecutionGraph
:
compiled = graph.compile()
# Returns: ExecutionGraph with dependency info
4. Execute
Run the graph:
from mesh import Executor, ExecutionContext, MemoryBackend
executor = Executor(compiled, MemoryBackend())
context = ExecutionContext(
graph_id="my-graph",
session_id="session-1",
chat_history=[],
variables={},
state={}
)
async for event in executor.execute("input", context):
print(event)
Graph Patterns
Sequential Flow
# A → B → C
graph.add_node("A", None, node_type="llm")
graph.add_node("B", None, node_type="llm")
graph.add_node("C", None, node_type="llm")
graph.add_edge("START", "A")
graph.add_edge("A", "B")
graph.add_edge("B", "C")
Parallel Branches
# A → [B, C] (both execute after A)
graph.add_node("A", None, node_type="llm")
graph.add_node("B", None, node_type="llm")
graph.add_node("C", None, node_type="llm")
graph.add_edge("START", "A")
graph.add_edge("A", "B")
graph.add_edge("A", "C")
Conditional Branching
from mesh.nodes import Condition
# A → [condition] → [B or C]
graph.add_node("A", None, node_type="llm")
graph.add_node("condition", [
Condition("success", lambda x: "success" in str(x), "B"),
Condition("failure", lambda x: "failure" in str(x), "C")
], node_type="condition")
graph.add_node("B", None, node_type="llm")
graph.add_node("C", None, node_type="llm")
graph.add_edge("START", "A")
graph.add_edge("A", "condition")
graph.add_edge("condition", "B")
graph.add_edge("condition", "C")
Multi-Input Nodes
# [A, B] → C (C waits for both A and B)
graph.add_node("A", None, node_type="llm")
graph.add_node("B", None, node_type="llm")
graph.add_node("C", None, node_type="llm")
graph.add_edge("START", "A")
graph.add_edge("START", "B")
graph.add_edge("A", "C")
graph.add_edge("B", "C") # C waits for both
Controlled Loops
Mesh supports loops with proper controls to prevent infinite execution:
Loop with Max Iterations
# Self-loop with fixed iteration count
graph.add_node("process", process_fn, node_type="tool")
graph.add_edge("START", "process")
graph.add_edge(
"process",
"process", # Loop back to itself
is_loop_edge=True,
max_iterations=10 # Run at most 10 times
)
Loop with Condition
# Loop until condition is met
def should_continue(state, output):
return output.get("value", 0) < 100
graph.add_node("increment", increment_fn, node_type="tool")
graph.add_edge("START", "increment")
graph.add_edge(
"increment",
"increment",
is_loop_edge=True,
loop_condition=should_continue # Exit when returns False
)
Loop with Both Controls
# Loop with condition AND max iterations for safety
graph.add_node("check", check_fn, node_type="tool")
graph.add_node("process", process_fn, node_type="tool")
graph.add_edge("START", "check")
graph.add_edge("check", "process")
graph.add_edge(
"process",
"check", # Loop back
is_loop_edge=True,
loop_condition=lambda state, output: not output.get("done", False),
max_iterations=50 # Safety limit
)
Loop Condition Signature:
def loop_condition(state: Dict, output: Dict) -> bool:
"""Return True to continue loop, False to exit."""
return some_check(state, output)
Key Requirements:
- Loop edges must have
is_loop_edge=True
- Must specify at least one:
loop_condition
ormax_iterations
- Loop conditions receive both shared state and node output
- Conditions that fail (raise exception) safely exit the loop
Execution Model
Mesh uses a queue-based execution model:
- Initialize queue with entry point
- Dequeue node and execute
- Emit events (streaming)
- Queue children based on dependencies
- Repeat until queue is empty
Dependency Resolution
When a node has multiple parents:
# A → C
# B → C (C waits for both A and B)
The executor:
- Tracks received inputs from parents
- Waits until all parents complete
- Combines inputs
- Executes node
Graph State
State flows through the graph via:
1. Node Outputs
Each node produces output that becomes input for children:
# Node A output: {"content": "Hello"}
# Node B receives: {"content": "Hello"}
2. Shared State
Persistent state across all nodes:
context = ExecutionContext(
graph_id="my-graph",
session_id="session-1",
chat_history=[],
variables={"user_id": "123"}, # Shared variables
state={"step_count": 0} # Shared state
)
3. Chat History
Accumulated conversation history:
context.chat_history = [
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"}
]
Best Practices
1. Use Descriptive Node IDs
# ❌ Bad
graph.add_node("n1", None, node_type="llm")
graph.add_node("n2", None, node_type="llm")
# ✅ Good
graph.add_node("analyzer", None, node_type="llm")
graph.add_node("summarizer", None, node_type="llm")
2. Keep Graphs Simple
# ✅ Good: 3-7 nodes
START → analyze → process → respond → END
# ⚠️ Consider splitting: 20+ nodes
# Too complex, hard to maintain
3. Validate Early
graph = StateGraph()
graph.add_node("step1", None, node_type="llm")
graph.add_node("step2", None, node_type="llm")
graph.add_edge("START", "step1")
graph.add_edge("step1", "step2")
graph.set_entry_point("step1")
try:
compiled = graph.compile() # Validates
except ValueError as e:
print(f"Graph invalid: {e}")
4. Use Variables for Flexibility
# Reference previous nodes
graph.add_node("step2", None, node_type="llm",
system_prompt="Based on , ...")
Common Errors
“No entry point set”
Problem: Forgot to call set_entry_point()
Solution:
graph.set_entry_point("first_node")
compiled = graph.compile()
“Cycle detected in graph”
Problem: Edge creates an uncontrolled cycle (no loop controls)
Solution: Mark the edge as a loop edge with proper controls:
# ❌ Bad: Uncontrolled cycle
graph.add_edge("A", "B")
graph.add_edge("B", "A") # Error: cycle!
# ✅ Good: Controlled loop
graph.add_edge("A", "B")
graph.add_edge(
"B",
"A",
is_loop_edge=True,
max_iterations=10 # Or use loop_condition
)
“Orphaned nodes detected”
Problem: Node not connected to START
Solution: Add edge from START or parent node:
graph.add_edge("START", "orphaned_node")
“Node not found”
Problem: Referencing non-existent node in edge
Solution: Ensure node exists before adding edge:
graph.add_node("node_a", None, node_type="llm") # Create first
graph.add_edge("START", "node_a") # Then reference
“Loop edge must have loop_condition or max_iterations”
Problem: Marked edge as is_loop_edge=True
but didn’t provide controls
Solution: Add at least one control mechanism:
# ❌ Bad: Loop edge without controls
graph.add_edge("A", "A", is_loop_edge=True)
# ✅ Good: With max_iterations
graph.add_edge("A", "A", is_loop_edge=True, max_iterations=10)
# ✅ Good: With condition
graph.add_edge("A", "A", is_loop_edge=True,
loop_condition=lambda s, o: o.get("count", 0) < 5)
# ✅ Best: With both for safety
graph.add_edge("A", "A", is_loop_edge=True,
loop_condition=lambda s, o: not o.get("done", False),
max_iterations=100)
Advanced Topics
Sub-Graphs (Future)
Coming in v2:
# Compose graphs
sub_graph = StateGraph()
# ... build sub_graph
main_graph = StateGraph()
main_graph.add_subgraph("processing", sub_graph)
Dynamic Graphs (Future)
Coming in v2:
# Modify graph during execution
executor.add_node_dynamically("new_node", ...)
See Also
- Nodes - Learn about node types
- Execution - Understand execution flow
- State Management - Managing state
- Variables - Variable resolution