> ## Documentation Index
> Fetch the complete documentation index at: https://launchdarkly.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Beyond n8n for workflow automation: agent graphs as your universal agent harness

export const BlogAuthor = ({date, avatar, alt, name}) => {
  return <div className="-mb-4">
      <p className="m-0 text-base font-semibold">
        <em>Published {date}</em>
      </p>
      <div className="flex items-center -mt-4">
        {avatar && <img src={avatar} alt={alt} className="h-8 w-8 rounded-full m-0" />}
        <p className="ml-[5px] text-lg">{name}</p>
      </div>
    </div>;
};

<View title="Developer" />

<View title="Federal docs" />

<View title="EU docs" />

<BlogAuthor date="March 20, 2026" avatar="/images/authors/scarlettattensil.png" alt="Portrait of Scarlett Attensil." name="by Scarlett Attensil" />

Hardcoded multi-agent orchestration is brittle: topology lives in framework-specific code, changes require redeploys, and bottlenecks are hard to see. [Agent graphs](/docs/home/agentcontrol/agent-graphs) externalize that topology into LaunchDarkly, while your application continues to own execution.

In this tutorial, you'll build a small multi-agent workflow, traverse it with the SDK, monitor per-node latency on the graph itself, and update a slow node's model without changing application code.

<Info>
  **How agent graphs work**

  * **Node** = AgentControl config (model, instructions, tools)
  * **Edge** = handoff metadata (routing contract you define)
  * **Graph** = topology (which nodes connect)
  * **Your app** = execution + interpretation

  LaunchDarkly provides graph structure, config, and observability. Your application owns execution semantics: you write the code that interprets edges and runs agents.
</Info>

<Frame caption="Agent graphs: visual orchestration with per-node metrics overlaid directly on your workflow">
  <img src="https://mintcdn.com/launchdarkly/ppwc2LYsjE8wwIC_/images/tutorials/agent-graphs/graph.png?fit=max&auto=format&n=ppwc2LYsjE8wwIC_&q=85&s=b93f55ba26d58b03f3fdd06365fbb02c" alt="Agent graph with monitoring" width="2650" height="1764" data-path="images/tutorials/agent-graphs/graph.png" />
</Frame>

## What you'll build

In this tutorial, you'll add agent graphs to an existing multi-agent workflow:

1. **Build a graph visually** in the LaunchDarkly UI
2. **Connect it to your code** with a few lines of SDK integration
3. **Run your agents** and watch the graph in action
4. **Monitor performance** with per-node latency and invocation tracking
5. **Fix a slow agent** by swapping models in the LaunchDarkly UI

By the end, you'll have a multi-agent system where topology metadata changes happen in the UI, picked up by your traversal code on the next request.

## Prerequisites

* LaunchDarkly account with AgentControl access ([sign up here](https://app.launchdarkly.com/signup))
* Python 3.9+
* An existing agent workflow (or use our [sample repo](https://github.com/launchdarkly-labs/devrel-agents-tutorial/tree/tutorial/agent-graphs))

## The problem with hardcoded orchestration

Every multi-agent framework handles orchestration differently:

```python lines wrap theme={null}
# LangGraph - topology hardcoded in graph setup
workflow = StateGraph(AgentState)
workflow.add_node("supervisor", supervisor_node)
workflow.add_node("security", security_node)
workflow.add_node("support", support_node)
workflow.set_entry_point("supervisor")
# Routing logic buried in node functions or conditional edges

# OpenAI Agents SDK - handoffs defined per agent
security_agent = Agent(name="Security", instructions="...")
support_agent = Agent(name="Support", instructions="...")
supervisor = Agent(
    name="Supervisor",
    handoffs=[security_agent, support_agent]  # Topology locked in code
)
```

The topology is scattered across code. Agent graphs make it visible: you view the entire workflow in one place, edit connections in the UI, and traverse it with graph-aware SDK methods.

## Why externalizing topology helps

If you've built multi-agent systems with [LangGraph](/docs/tutorials/agents-langgraph), OpenAI Swarm, or Strands, you've run into the same limitations:

* **Config duplication**: Agent definitions scattered across framework-specific formats
* **Silent failures**: An agent times out and you don't know until users complain
* **No topology visibility**: The workflow exists only in code
* **Custom observability**: Getting consistent per-agent metrics means reconciling different trace formats and data schemas across frameworks

<Info>
  **Deep dive on orchestrators**

  For a detailed comparison of LangGraph, OpenAI Swarm, and Strands, read [Compare AI orchestrators](/docs/tutorials/ai-orchestrators). Agent graphs work with multiple agent frameworks.
</Info>

Agent graphs solve these by giving you a **visual graph builder** where you:

* **View your entire workflow** at a glance, not buried in code
* **Monitor per-node metrics** overlaid directly on the graph (latency, invocations, tool calls)
* **Add or remove agents** without changing traversal logic, provided your runtime supports the node's tools and output contract
* **Inspect routing logic** on edges, with handoff data visible in the UI
* **Use graph-aware SDK methods** like `is_terminal()`, `get_edges()`, and `root()` instead of manual tracking

## Step 1: Create a project and configs

Agent graphs live in a LaunchDarkly [project](/docs/home/account/project) alongside your configs. Create a dedicated project for this tutorial, then seed it with an [AgentControl config](/docs/home/agentcontrol/agents) for each agent. The bootstrap script below handles config setup. If you already have a project, skip to [Step 2](#step-2-build-the-graph-in-the-ui).

<Info>
  **Create a tutorial project first**

  Ask your coding assistant to create the project with the LaunchDarkly MCP server (or the [`projects` agent skill](https://github.com/launchdarkly/ai-tooling/tree/main/skills/agentcontrol/projects)), or [create one manually](/docs/home/account/project#create-projects) under **Account settings > Projects**. Then copy its SDK key and project key into your `.env`.
</Info>

<Info>
  **New to AgentControl?**

  Use the [AgentControl quickstart](/docs/home/agentcontrol/quickstart) or run the bootstrap script in our [sample repo](https://github.com/launchdarkly-labs/devrel-agents-tutorial) to seed your project with the three configs:

  ```bash lines wrap theme={null}
  git clone https://github.com/launchdarkly-labs/devrel-agents-tutorial
  cd devrel-agents-tutorial
  git checkout tutorial/agent-graphs
  uv sync
  cp .env.example .env  # Add your LD_PROJECT_KEY, LD_SDK_KEY, LD_API_KEY, OPENAI_API_KEY
  cd bootstrap && uv run python create_configs.py
  ```

  The bootstrap script creates the configs in the project you set in `LD_PROJECT_KEY`.
</Info>

For this tutorial, we'll use three configs:

* **supervisor-agent**: Orchestrates the workflow and routes queries based on PII pre-screening
* **security-agent**: Detects and redacts personally identifiable information (PII)
* **support-agent**: Answers questions using dynamically loaded [tools](/docs/home/agentcontrol/tools) (search, RAG)

## Step 2: Build the graph in the UI

This is where agent graphs diverge from code-based orchestration. Instead of writing `add_edge()` calls, you'll **view your topology** and modify it visually.

Open the LaunchDarkly UI and navigate to **AI > Agent graphs**.

1. The first-time setup wizard appears. Since you already created configs in Step 1, expand **Create a graph** at the bottom.

<Frame caption="First-time setup wizard. Expand 'Create a graph' since you already have configs.">
  <img src="https://mintcdn.com/launchdarkly/ppwc2LYsjE8wwIC_/images/tutorials/agent-graphs/create-first-graph-wizard.png?fit=max&auto=format&n=ppwc2LYsjE8wwIC_&q=85&s=a2d19988950b2b6f6786f1fa08273e4e" alt="First-time agent graph wizard" width="1130" height="1118" data-path="images/tutorials/agent-graphs/create-first-graph-wizard.png" />
</Frame>

2. Name your graph `chatbot-flow` and click **Create graph**.

<Frame caption="Creating your first agent graph in the LaunchDarkly UI.">
  <img src="https://mintcdn.com/launchdarkly/ppwc2LYsjE8wwIC_/images/tutorials/agent-graphs/create-first-graph.png?fit=max&auto=format&n=ppwc2LYsjE8wwIC_&q=85&s=f6823238b6e46b88ff5ade20f0af3b72" alt="Creating your first agent graph" width="3392" height="1666" data-path="images/tutorials/agent-graphs/create-first-graph.png" />
</Frame>

3. Add your first node: click **Add node** and select `supervisor-agent`
4. Add `security-agent` and `support-agent` as nodes

<Frame caption="Adding the security-agent node to the graph.">
  <img src="https://mintcdn.com/launchdarkly/ppwc2LYsjE8wwIC_/images/tutorials/agent-graphs/add-security.png?fit=max&auto=format&n=ppwc2LYsjE8wwIC_&q=85&s=89b64b7645870a12bba468a4e20132ec" alt="Adding security agent" width="3364" height="1650" data-path="images/tutorials/agent-graphs/add-security.png" />
</Frame>

<Frame caption="Adding the support-agent node to complete the workflow.">
  <img src="https://mintcdn.com/launchdarkly/ppwc2LYsjE8wwIC_/images/tutorials/agent-graphs/add-support.png?fit=max&auto=format&n=ppwc2LYsjE8wwIC_&q=85&s=3b717b9defeaa87196f73ccb20e8bd85" alt="Adding support agent" width="3382" height="1658" data-path="images/tutorials/agent-graphs/add-support.png" />
</Frame>

5. Draw edges: drag from `supervisor-agent` to both child agents
6. Add handoff data to each edge to define routing logic:

**supervisor-agent → security-agent**:

```json lines wrap theme={null}
{
  "action": "sanitize",
  "reason": "PII detected",
  "route": "security"
}
```

<Frame caption="Edge from supervisor to security with route: security handoff data.">
  <img src="https://mintcdn.com/launchdarkly/ppwc2LYsjE8wwIC_/images/tutorials/agent-graphs/edge-pii.png?fit=max&auto=format&n=ppwc2LYsjE8wwIC_&q=85&s=2c3f131b6b48b786d230dabb76f8df46" alt="PII detected edge" width="3362" height="1652" data-path="images/tutorials/agent-graphs/edge-pii.png" />
</Frame>

**supervisor-agent → support-agent**:

```json lines wrap theme={null}
{
  "action": "direct",
  "reason": "Clean input",
  "route": "support"
}
```

<Frame caption="Edge from supervisor to support with route: support handoff data.">
  <img src="https://mintcdn.com/launchdarkly/ppwc2LYsjE8wwIC_/images/tutorials/agent-graphs/edge-clean.png?fit=max&auto=format&n=ppwc2LYsjE8wwIC_&q=85&s=cdf28a68e8cb3a8c2f1a62e4d774b5d0" alt="Clean edge" width="3380" height="1650" data-path="images/tutorials/agent-graphs/edge-clean.png" />
</Frame>

**security-agent → support-agent**:

```json lines wrap theme={null}
{
  "action": "proceed",
  "reason": "Input sanitized",
  "route": "continue"
}
```

<Frame caption="Edge from security to support with route: continue handoff data.">
  <img src="https://mintcdn.com/launchdarkly/ppwc2LYsjE8wwIC_/images/tutorials/agent-graphs/edge-redacted.png?fit=max&auto=format&n=ppwc2LYsjE8wwIC_&q=85&s=73611bc63306d2bc16bc2b19a9282831" alt="Redacted edge" width="3374" height="1654" data-path="images/tutorials/agent-graphs/edge-redacted.png" />
</Frame>

Notice what you're seeing: the entire workflow topology in one view. This graph *is* your architecture diagram, always current. Each node shows which config variation it serves. The edges show routing logic that would otherwise be buried in conditional statements. When you need to add a new agent or change routing, you do it here, not in code.

<Info>
  **The graph defines structure, your code defines behavior**

  LaunchDarkly doesn't execute your graph. It provides:

  * **Topology**: Which nodes exist and how they connect
  * **Handoff metadata**: Whatever JSON you put on edges
  * **Per-node config**: Model, instructions, tools for each agent

  Your code:

  * Decides which edges to follow based on agent decisions
  * Interprets handoff data however you want (the schema is yours)
  * Executes the actual agents

  The handoff JSON is arbitrary metadata. You define the schema, you interpret it. LaunchDarkly stores and delivers it.
</Info>

## Step 3: Add the SDK to your project

Install the LaunchDarkly AI SDK:

```bash lines wrap theme={null}
uv add launchdarkly-server-sdk launchdarkly-server-sdk-ai
```

Initialize the clients in your code:

```python lines wrap theme={null}
# config_manager.py - Initialize LaunchDarkly clients
def _initialize_launchdarkly_client(self):
    """Initialize LaunchDarkly client and AI client"""
    config = ldclient.Config(self.sdk_key)
    ldclient.set_config(config)
    self.ld_client = ldclient.get()

    # Block until client is initialized (max 10 seconds)
    self.ld_client.start_wait(10)

    if not self.ld_client.is_initialized():
        raise RuntimeError("LaunchDarkly client initialization failed")

    self.ai_client = LDAIClient(self.ld_client)
```

Build a context for targeting and tracking:

```python lines wrap theme={null}
# config_manager.py - Build context for targeting
def build_context(self, user_id: str, user_context: dict = None) -> Context:
    """Build a LaunchDarkly context with consistent attributes."""
    context_builder = Context.builder(user_id).kind('user')

    if user_context:
        for key, value in user_context.items():
            context_builder.set(key, value)

    return context_builder.build()
```

## Step 4: Integrate with your framework

This section walks through the integration code, starting with the building block (what runs at each node), then showing how nodes are orchestrated.

### The generic agent pattern

The key to dynamic execution is `create_generic_agent`. Every node uses the same implementation, with no agent registry and no hardcoded agent types:

```python expandable lines wrap theme={null}
# agents/generic_agent.py
def create_generic_agent(agent_config, config_manager, valid_routes: List[str] = None):
    """Create a generic agent from a config."""

    class GenericAgent:
        def __init__(self):
            self.valid_routes = valid_routes or []

        async def ainvoke(self, state: dict) -> dict:
            """Execute the agent using config."""
            if not agent_config.enabled:
                return {"response": "", "_skipped": True}

            # Create model from config
            model = create_model_for_config(
                provider=agent_config.provider.name,
                model=agent_config.model.name,
                config_manager=config_manager
            )

            # Load tools from config
            tools = create_dynamic_tools_from_launchdarkly(agent_config)

            # Get instructions from config
            instructions = agent_config.instructions or "Process the input."

            # Inject route options into instructions
            if self.valid_routes:
                route_instruction = f"\n\nSelect one of these routes: {self.valid_routes}. Return: {{\"route\": \"<selected_route>\"}}"
                instructions = instructions + route_instruction

            # Execute and extract routing decision
            result = await self._execute(model, instructions, tools, state)
            result["routing_decision"] = self._extract_route(result.get("response", ""))

            tracker = agent_config.create_tracker()
            tracker.track_success()
            return result

    return GenericAgent()
```

<Info>
  **Why this enables adding agents without code changes**

  The generic agent pattern means:

  * **No agent registry**: Every node uses the same `create_generic_agent` function
  * **Config-driven behavior**: Model, instructions, and tools all come from LaunchDarkly
  * **Dynamic routing**: Valid routes are injected from graph edges, not hardcoded
  * **Minimal code changes**: Add a new agent in LaunchDarkly, create its config, add it to your graph, and it works, provided your runtime supports the node's tools and output contract
</Info>

### The AgentService class

The `AgentService` class is the entry point for processing messages through your agent graph:

```python expandable lines wrap theme={null}
# api/services/agent_service.py
class AgentService:
    """Multi-Agent Orchestration using LaunchDarkly Agent Graph."""

    def __init__(self):
        self.config_manager = ConfigManager()
        self.config_manager.flush()

    async def process_message(
        self,
        user_id: str,
        message: str,
        user_context: dict = None
    ) -> ChatResponse:
        """Process message using LaunchDarkly Agent Graph."""
        result = await self._execute_graph(
            graph_key=os.getenv("AGENT_GRAPH_KEY", "chatbot-flow"),
            user_id=user_id.strip() or "anonymous",
            user_input=message,
            user_context=user_context or {}
        )

        return ChatResponse(
            response=result.get("final_response", ""),
            tool_calls=result.get("tool_calls", []),
            # ... other fields
        )
```

### Executing the graph

The `_execute_graph` method fetches the graph from LaunchDarkly and uses `traverse()` with skip logic for conditional routing:

```python expandable lines wrap theme={null}
# api/services/agent_service.py
async def _execute_graph(
    self,
    graph_key: str,
    user_id: str,
    user_input: str,
    user_context: dict = None
) -> Dict[str, Any]:
    """Execute agents using SDK's traverse() with skip logic."""
    ld_context = self.config_manager.build_context(user_id, user_context)
    graph = self.config_manager.ai_client.agent_graph(graph_key, ld_context)

    if not graph.is_enabled():
        raise ValueError(f"Agent Graph '{graph_key}' is not enabled")

    ctx = {
        "user_input": user_input,
        "messages": [HumanMessage(content=user_input)],
        "processed_input": user_input,
        "final_response": "",
        "tool_calls": [],
        # Skip logic: track which nodes should execute
        "_routed_to": {graph.root().get_key()},
        "_path": [],
        "_prev_key": None,
    }

    tracker = graph.create_tracker()

    # Define the node callback (see next section)
    def execute_node(node, exec_ctx):
        # ... node execution logic
        pass

    # Use SDK's traverse() - it handles traversal order
    graph.traverse(execute_node, ctx)

    # Track graph completion
    if tracker:
        tracker.track_path(ctx.get("_path", []))
        tracker.track_invocation_success()

    return ctx
```

### Skip logic for conditional routing

The `execute_node` callback implements skip logic, the core pattern that enables conditional routing:

```python expandable lines wrap theme={null}
# api/services/agent_service.py - inside _execute_graph
def execute_node(node, exec_ctx):
    """Execute a single node if it was routed to."""
    key = node.get_key()

    # Skip logic: only execute if parent routed to this node
    if key not in exec_ctx.get("_routed_to", set()):
        return {"_skipped": True}

    exec_ctx["_path"].append(key)

    node_tracker = node.get_config().create_tracker()
    node_tracker.track_success()
    if tracker and exec_ctx.get("_prev_key"):
        tracker.track_handoff_success(exec_ctx["_prev_key"], key)

    # Get edges and valid routes for this node
    edges = node.get_edges()
    valid_routes = [e.handoff.get("route") for e in edges if e.handoff and e.handoff.get("route")]

    # Execute agent with config from this node
    agent = create_generic_agent(node.get_config(), self.config_manager, valid_routes=valid_routes)
    result = _run_async(agent.ainvoke(exec_ctx))

    # Track tool calls
    if result.get("tool_calls"):
        for tool in result["tool_calls"]:
            node_tracker.track_tool_call(tool)

    # Route to next node: add to _routed_to set
    if edges:
        next_key = self._select_next_node(node, edges, result, tracker)
        if next_key:
            exec_ctx["_routed_to"].add(next_key)

    exec_ctx["_prev_key"] = key
    return result
```

<Info>
  **How skip logic enables conditional routing**

  The `_routed_to` set tracks which nodes should execute:

  1. **Start**: Add root node to `_routed_to`
  2. **traverse() visits each node**: If node is in `_routed_to`, execute it; otherwise skip
  3. **After execution**: Add the next node (based on routing decision) to `_routed_to`

  This enables conditional routing: the supervisor routes to either security OR support, and only the chosen path executes.
</Info>

### Routing between nodes

The `_select_next_node` method determines which node to route to based on the agent's routing decision:

```python expandable lines wrap theme={null}
# api/services/agent_service.py
def _select_next_node(self, node, edges, result: dict, tracker=None):
    """Select next node key based on routing decision."""
    routing = result.get("routing_decision", "").lower().strip() if result.get("routing_decision") else None

    # Build route map: route -> target_config
    route_map = {}
    for edge in edges:
        route = (edge.handoff.get("route", "") if edge.handoff else "").lower().strip()
        if route:
            route_map[route] = edge.target_config

    # Exact match
    if routing and routing in route_map:
        return route_map[routing]
    elif routing:
        if tracker:
            target = edges[0].target_config if edges else None
            tracker.track_handoff_failure(node.get_key(), target)

    # Default: first edge
    if edges:
        return edges[0].target_config

    return None
```

The key insight: your graph topology comes from LaunchDarkly, not hardcoded orchestration. Change the graph in the UI, and your code picks up the new structure on the next request.

## Step 5: Run it

With the `AgentService` wired up (as shown in Step 4), you can now process messages through your agent graph. The service handles:

1. Building the LaunchDarkly context for targeting
2. Fetching the graph and executing nodes via `traverse()`
3. Tracking metrics for monitoring
4. Returning the final response

Test it by sending a message:

```python lines wrap theme={null}
service = AgentService()
response = await service.process_message(
    user_id="user-123",
    message="What's the status of my order?",
    user_context={"plan": "premium"}
)
print(response.response)
```

Now go back to the LaunchDarkly UI. Add a new node or change an edge. Run your code again. Topology changes are picked up by your traversal code on subsequent SDK evaluations.

## Step 6: Monitor agent performance

This is the key differentiator: monitoring happens **on the graph itself**, not in a separate view. You see metrics overlaid on the same visual topology you built, so bottlenecks are immediately obvious.

The sample repo includes full instrumentation: calls to `tracker.track_success()`, `tracker.track_error()`, and `tracker.track_tool_call()` in the agent execution path. After running some traffic, open your agent graph to view the results.

Navigate to **AI > Agent graphs > chatbot-flow**. A metrics bar at the top of the graph view lets you toggle different metrics on and off.

### Metrics on the graph

Here's what makes this different from traditional APM: the metrics appear **directly on your workflow visualization**. No mental mapping between a separate view and your code. No correlating trace IDs. The slow node lights up on the graph.

Turn on **Latency** to see duration data overlaid directly on your graph:

* **Total duration**: The combined time for the entire graph invocation
* **Per-node duration**: How long each individual agent takes

Turn on **Invocations** to see how often each node is reached. This reveals which paths your users take most frequently. In a routing graph, you'll quickly see whether most queries go through security or skip directly to support.

Turn on **Tool calls** to view the average number of tool invocations per node. If an agent is calling tools excessively, you'll spot it here.

### Monitoring page

Click **Monitoring** to see all metrics over time. This view shows:

* **Latency trends**: Duration per node over hours, days, or weeks
* **Invocation patterns**: Traffic flow through your graph
* **Tool call breakdown**: Which specific tools are being called and how often

<Frame caption="Node-level metrics broken down by agent, showing invocations, tool calls, and latency over time">
  <img src="https://mintcdn.com/launchdarkly/ppwc2LYsjE8wwIC_/images/tutorials/agent-graphs/monitoring.png?fit=max&auto=format&n=ppwc2LYsjE8wwIC_&q=85&s=66432227af232c590a040ba42320197d" alt="Monitoring view" width="2920" height="1342" data-path="images/tutorials/agent-graphs/monitoring.png" />
</Frame>

<Info>
  **Instrument tool tracking**

  To see which specific tools are called, you need to track them in your code using the tracker. The SDK sends this data to LaunchDarkly, which displays it in the monitoring view.
</Info>

### Generate traffic to see metrics

Run the traffic generator from the sample repo to send queries through your graph:

```bash lines wrap theme={null}
uv run python tools/traffic_generator.py --queries 20 --delay 2
```

This sends a mix of queries (some with PII, some without) to exercise both the security and support paths. After a few minutes, you'll see metrics populate on the graph.

### Detecting a slow agent

With traffic flowing, suppose the security-agent starts averaging 5 seconds per call. With latency metrics enabled on the graph, you see it immediately: the security-agent node shows a high duration value while other nodes stay fast.

The invocation numbers also tell a story. If security-agent shows 50 invocations and support-agent shows 80, you know \~30 queries are bypassing security (the clean path). This helps you understand whether the slow agent is affecting most users or just a subset.

Without agent graphs, you'd need custom logging, Datadog queries, and manual correlation. With agent graphs, you spot the problem in 30 seconds.

## Step 7: Fix without deploying

The security-agent is slow because it's using `claude-sonnet-4-6` for PII detection. A smaller, faster model may be sufficient for this task.

In the LaunchDarkly UI, update the `security-agent` variation:

* Change model from `Anthropic.claude-sonnet-4-6` to `Anthropic.claude-haiku-4-5-20251001`

Or use [Agent Skills](/docs/tutorials/agent-skills-quickstart) to make the change from your coding assistant:

```lines wrap theme={null}
The security-agent variation is averaging 5 seconds.
Change the model to claude-haiku-4-5-20251001.
```

No code changes. No deploy. Changes are picked up on subsequent SDK evaluations.

Run the traffic generator again and watch the latency drop.

### What just happened

1. **Traffic generator** sent queries through the graph
2. **Monitoring** showed the slow agent on the graph
3. **Model swap** happened in the UI (or via Agent Skills)
4. **Your code** automatically used the new configuration

No deploys. No PRs. The fix is live.

## OpenAI Agents SDK integration (conceptual)

Agent graphs work with multiple frameworks. This conceptual example shows how the pattern translates to OpenAI Agents SDK:

```python expandable lines wrap theme={null}
# Conceptual example showing how Agent Graph SDK methods work with OpenAI Agents
from agents import Agent, Runner

def handle_traversal(node, state):
    config = node.get_config()
    tracker = config.create_tracker()
    edges = node.get_edges()

    # Child agents are already in state (reverse traversal builds bottom-up)
    handoffs = [state[edge.target_config] for edge in edges]

    def on_handoff(ctx):
        # Track handoff events
        return ctx

    return Agent(
        name=config.key,
        instructions=config.instructions,
        handoffs=handoffs,
        on_handoff=on_handoff,
    )

if agent_graph.is_enabled():
    root = agent_graph.reverse_traverse(handle_traversal, {})

result = await Runner.run(root, "Tell me about your engineering team")
```

Same graph definition, adapted to each framework's execution model. The topology metadata lives in LaunchDarkly; your code interprets and executes it.

## Best practices

**Start simple**: Begin with a linear graph (A → B → C) before adding conditional routing.

**Use handoff data for context passing**: Include metadata like action type, reason, or state that the next agent needs to continue the workflow.

**Track everything**: Call `tracker.track_success()` and `tracker.track_error()` in every node for complete visibility. Use `node.get_config().create_tracker().track_tool_call(tool_name)` to track which tools agents invoke.

**Test with targeting**: Use LaunchDarkly [targeting](/docs/home/agentcontrol/target) to route test users to experimental graph configurations.

**Handle missing edges**: Decide what happens when no edge matches a routing decision or when you turn off a target node. Recommend: fail closed, log diagnostics, and track routing failures.

**Keep execution state request-scoped**: Store execution state inside the context object (`ctx`) passed through traversal, not in instance-level variables. Treat graph traversal as request-scoped to avoid concurrency issues.

## What you've built

You now have a multi-agent system where:

* **Graph topology** is externalized and self-documenting
* **Routing logic** is visible on edges, not buried in code
* **Monitoring** appears on the graph itself, not a separate view
* **Node-level control** lets you disable a single agent without touching others, provided your executor checks node availability
* **Multiple frameworks** can consume the same graph metadata

When you spot a slow agent in monitoring, swap the model in the LaunchDarkly UI without a deploy.

## Next steps

* **[Agent graphs](/docs/home/agentcontrol/agent-graphs)**: SDK methods for `traverse`, `reverse_traverse`, `get_edges()`, and handoff data
* **[AgentControl documentation](/docs/home/agentcontrol)**: Learn more about variations, targeting, and experiments
* **[Agent Skills tutorial](/docs/tutorials/agent-skills-quickstart)**: Manage configs from your coding assistant
* **[Monitor config performance](/docs/home/agentcontrol/monitor)**: Deep dive into metrics and monitoring views
* **[Sample repository](https://github.com/launchdarkly-labs/devrel-agents-tutorial/tree/tutorial/agent-graphs)**: Complete code from this tutorial
* **[Offline Evaluation of RAG-Grounded Answers](/docs/tutorials/offline-evals)**: Add a regression-testing layer for the graph's RAG-bearing nodes
* **[Smart AI Agent Targeting with MCP Tools](/docs/tutorials/multi-agent-mcp-targeting)**: Layer plan, geo, and tier targeting on top of your graph variations
* **[Evaluate LLM code generation with LLM-as-judge evaluators](/docs/tutorials/custom-evals-claude-code)**: Attach custom judges to individual nodes for per-agent quality scoring
* **[Building Framework-Agnostic AI Swarms](/docs/tutorials/ai-orchestrators)**: Run the same graph topology across LangGraph, Strands, and OpenAI Swarm
* **[Proving ROI with data-driven AI agent experiments](/docs/guides/experimentation/ai-experiments-roi)**: A/B test graph variations to prove which routing or model choice wins

## Conclusion

Hardcoded orchestration was fine when you had one agent. With multi-agent systems, it becomes a liability. Every change requires a deploy. Every incident requires a developer.

Agent graphs flip this. Define your workflow in LaunchDarkly, integrate it with your framework, and fix many problems without touching code. Your agents become as dynamic as your feature flags.

Ready to stop hardcoding? [Get started with AgentControl](https://app.launchdarkly.com/signup) and create your first agent graph.
