> ## 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.

# Getting started with LangGraph and AgentControl configs

This guide shows how to integrate LangGraph agent workflows with LaunchDarkly AgentControl. Using AgentControl configs with LangGraph lets you manage agent instructions, model configuration, and parameters outside of your application code.

This guide uses agent mode for LangGraph workflows. Agent mode uses a single `instructions` string rather than a messages array, which maps directly to LangGraph's agent prompts. To learn more, read [Agents](/docs/home/agentcontrol/agents).

<Tip>
  **New to AgentControl?**

  If you're new to AgentControl, start with the [Quickstart](/docs/home/agentcontrol/quickstart) and return to this guide when you are ready for a LangGraph-specific example.

  To learn more about AgentControl-specific SDKs, read [AI SDKs](/docs/sdk/ai). For Python-specific details, read the [Python AI SDK reference](/docs/sdk/ai/python).
</Tip>

## Prerequisites

To complete this guide, you must have the following prerequisites:

* A LaunchDarkly account, including:
  * A LaunchDarkly [SDK key](/docs/home/account/environment/keys#view-or-copy-sdk-credentials) for your environment.
  * A member role that allows [AgentControl actions](/docs/home/account/roles/role-actions#agentcontrol-config-actions). The LaunchDarkly project admin, maintainer, and developer project roles, as well as the admin and owner base roles, include this ability. To learn more about LaunchDarkly roles, read [Roles](/docs/home/account/roles).
* A Python 3.10+ or Node.js 20+ development environment.
* LangGraph installed in your application.
* An API key for your chosen model provider (OpenAI, Anthropic, or another supported provider).

## Concepts

Before you begin, review these key concepts.

### LangGraph agents

LangGraph provides a framework for building agent workflows as directed graphs. The `create_agent` function (in `langchain.agents`; replaces the deprecated `langgraph.prebuilt.create_react_agent` in LangGraph 1.0+) creates a ReAct-style agent that can use tools and maintain state across conversation turns. Agents receive a system prompt that defines their behavior and capabilities.

### Agent mode AgentControl configs

Agent mode AgentControl configs use an `instructions` field instead of a messages array. This single instruction string serves as the system prompt for your agent. Agent mode is ideal for:

* Multi-step agent workflows
* Tool-using agents
* Persistent agent sessions

### The agent\_config function

The `agent_config` function retrieves the AgentControl config variation for a given context. It returns an `AIAgentConfig` object that includes the customized instructions, model configuration, and a `create_tracker()` (Python) or `createTracker()` (Node.js) factory method that returns a tracker for recording metrics. Call `agent_config` each time you create an agent so LaunchDarkly can evaluate targeting and return the current configuration.

## Step 1: Install dependencies

Install the LaunchDarkly SDKs and LangGraph packages.

<CodeGroup>
  ```bash title="Python" lines wrap theme={null}
  pip install "launchdarkly-server-sdk-ai>=0.20.0" "launchdarkly-server-sdk-ai-langchain>=0.7.0" langgraph langchain langchain-core python-dotenv
  ```

  ```bash title="Node.js (server-side)" lines wrap theme={null}
  npm install @launchdarkly/node-server-sdk "@launchdarkly/server-sdk-ai@^0.20.0" "@launchdarkly/server-sdk-ai-langchain@^0.5.5" @langchain/langgraph @langchain/core langchain zod dotenv
  ```
</CodeGroup>

Install the LangChain provider package for your model. Common provider packages include:

* `langchain-openai` for OpenAI models
* `langchain-anthropic` for Anthropic models
* `langchain-google-genai` for Google Gemini models

## Step 2: Create an AgentControl config in LaunchDarkly

Create an AgentControl config in agent mode to store your agent configuration.

To create an AgentControl config:

1. In the left sidebar, click **Create** and select **Config**.
2. In the "Create config" dialog, select **Agent**.
3. Enter a name for your AgentControl config, for example, "LangGraph Agent."
4. Click **Create**.

<Frame caption="The &#x22;Create&#x22; menu options.">
  <img src="https://mintcdn.com/launchdarkly/-b7nPh0oyigf6iW7/images/auto/create-menu.auto.png?fit=max&auto=format&n=-b7nPh0oyigf6iW7&q=85&s=5b3fe40c4dd161b2972de2334ff19c0a" alt="The &#x22;Create&#x22; menu options." width="1180" height="440" data-path="images/auto/create-menu.auto.png" />
</Frame>

Then, create a variation:

1. On the **Variations** tab, replace "Untitled variation" with a variation name, such as "GPT-4o Agent".
2. Click **Select a model** and choose the `gpt-4o` OpenAI model.
3. Click **Parameters** and set `temperature` to `0.7` and `max_tokens` to `2000`.
4. In the **Instructions** field, enter your agent's system prompt:

```lines wrap theme={null}
You are a helpful assistant that can perform calculations and check weather. Use the available tools to provide accurate information. Always explain your reasoning step by step.
```

5. Click **Review and save**.

<Frame caption="A completed variation with model configuration and instructions.">
  <img src="https://mintcdn.com/launchdarkly/-b7nPh0oyigf6iW7/images/auto/ai-config-variation-complete.auto.png?fit=max&auto=format&n=-b7nPh0oyigf6iW7&q=85&s=e6b80ed68d6edb05d7f31ee72e6b4974" alt="A completed variation with model configuration and instructions." width="2070" height="638" data-path="images/auto/ai-config-variation-complete.auto.png" />
</Frame>

## Step 3: Set up targeting rules

Configure targeting rules to control which users receive the AgentControl config variation.

To set up the default rule:

1. Select the **Targeting** tab for your AgentControl config.
2. In the "Default rule" section, click **Edit**.
3. Configure the default rule to serve your variation, such as "GPT-4o Agent".
4. Click **Review and save**.

<Frame caption="The default targeting rule configured to serve a variation.">
  <img src="https://mintcdn.com/launchdarkly/hutarVphEq2dY_zb/images/auto/guide-ai-config-model-config-update-default-targeting-rule.auto.png?fit=max&auto=format&n=hutarVphEq2dY_zb&q=85&s=b92bcc5a507600be0fd944fbd203113e" alt="The default targeting rule configured to serve a variation." width="2074" height="1138" data-path="images/auto/guide-ai-config-model-config-update-default-targeting-rule.auto.png" />
</Frame>

The AgentControl config is enabled by default. After you add the integration code to your application, LaunchDarkly serves the configured variation to your users.

## Step 4: Integrate LangGraph with AgentControl configs

The integration involves these key steps:

1. Define the tools your agent can call.
2. Initialize the LaunchDarkly SDK and AI client.
3. Get the agent config using `agent_config()` (Python) or `aiClient.agentConfig()` (Node.js).
4. Build a LangChain model from the AgentControl config using the LaunchDarkly LangChain provider.
5. Create a LangGraph ReAct agent with a `MemorySaver` checkpointer.
6. Invoke the agent and track metrics with the config's tracker.

Define the agent's tools.

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  from langchain_core.tools import tool


  @tool
  def get_order_status(order_id: str) -> str:
      """Look up the status of a customer order by order ID."""
      orders = {
          "ORD-123": "Shipped — arrives Thursday",
          "ORD-456": "Processing — estimated ship date: tomorrow",
          "ORD-789": "Delivered on Monday",
      }
      return orders.get(order_id, f"No order found with ID {order_id}")


  # Map tool keys (matching the LaunchDarkly tool keys) to local handlers. The agent
  # build step below resolves the active tool list from `agent_config.model.parameters['tools']`
  # so detaching `get_order_status` from the variation in LaunchDarkly takes effect on
  # the next agent invocation, with no code change.
  TOOL_REGISTRY = {"get_order_status": get_order_status}
  ```

  ```typescript title="Node.js" expandable lines wrap theme={null}
  import { tool } from '@langchain/core/tools';
  import { z } from 'zod';

  // tool() types from @langchain/core are deep; cast to any to avoid TS2589
  const getOrderStatus = (tool as any)(
    async ({ order_id }: { order_id: string }) => {
      const orders: Record<string, string> = {
        'ORD-123': 'Shipped — arrives Thursday',
        'ORD-456': 'Processing — estimated ship date: tomorrow',
        'ORD-789': 'Delivered on Monday',
      };
      return orders[order_id] || `No order found with ID ${order_id}`;
    },
    {
      name: 'get_order_status',
      description: 'Look up the status of a customer order by order ID',
      schema: z.object({
        order_id: z.string().describe('The order ID to look up'),
      }),
    },
  );

  // Map tool keys (matching the LaunchDarkly tool keys) to local handlers. The agent
  // build step below resolves the active tool list from `agentConfig.model?.parameters?.tools`
  // so detaching `get_order_status` from the variation in LaunchDarkly takes effect on
  // the next agent invocation, with no code change.
  const TOOL_REGISTRY: Record<string, any> = { get_order_status: getOrderStatus };
  ```
</CodeGroup>

Initialize the LaunchDarkly SDK and AI client, fetch the agent config, build the LangChain model with `create_langchain_model` (Python) or `createLangChainModel` (Node.js), and create the ReAct agent.

The provider reads the model name, provider, and all parameters (temperature, max tokens, and others) from the variation, maps LaunchDarkly provider names to LangChain equivalents — for example, `"gemini"` to `"google_genai"` — and returns a configured chat model. The same AgentControl config key can serve OpenAI, Anthropic, or any other provider-backed variation from the same code path.

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  import os
  import ldclient
  from ldclient import Context
  from ldclient.config import Config
  from ldai.client import LDAIClient
  from ldai.providers.types import LDAIMetrics
  from ldai_langchain import (
      create_langchain_model,
      get_tool_calls_from_response,
      sum_token_usage_from_messages,
  )
  from langchain_core.tools import tool
  from langchain.agents import create_agent
  from langgraph.checkpoint.memory import MemorySaver


  ldclient.set_config(Config(os.environ.get("LAUNCHDARKLY_SDK_KEY")))

  ai_client = LDAIClient(ldclient.get())

  context = Context.builder("user-123").kind("user").name("Sandy").build()

  # Pass a default for improved resiliency when the AgentControl config is unavailable
  # or LaunchDarkly is unreachable; omit for a disabled default.
  # Example:
  #   from ldai import AIAgentConfigDefault
  #   default = AIAgentConfigDefault(
  #       enabled=True,
  #       model={"name": "gpt-5"},
  #       provider={"name": "openai"},
  #       instructions="You are a helpful assistant.",
  #   )
  #   agent_config = ai_client.agent_config("langgraph-agent", context, default)
  agent_config = ai_client.agent_config("langgraph-agent", context)

  # create_langchain_model reads agent_config.model.name / .parameters and picks the
  # right chat model class (OpenAI, Anthropic, …) with no per-provider branching.
  llm = create_langchain_model(agent_config)

  # Resolve the agent's tool list from the active variation. LaunchDarkly returns
  # attached tools under `parameters.tools` in OpenAI function shape; we only need
  # the names to look up the local handlers from TOOL_REGISTRY.
  ld_tool_params = (agent_config.model.to_dict().get("parameters") or {}).get("tools") or []
  resolved_tools = [
      TOOL_REGISTRY[t["name"]] for t in ld_tool_params if t["name"] in TOOL_REGISTRY
  ]

  # MemorySaver gives the ReAct agent short-term memory per thread_id —
  # follow-up turns on the same thread see the earlier conversation.
  checkpointer = MemorySaver()
  agent = create_agent(
      llm,
      resolved_tools,
      system_prompt=agent_config.instructions,
      checkpointer=checkpointer,
  )
  ```

  ```typescript title="Node.js" expandable lines wrap theme={null}
  import { init, LDClient, LDContext } from '@launchdarkly/node-server-sdk';
  import { initAi, type LDAIClient, type LDAIAgentConfig } from '@launchdarkly/server-sdk-ai';
  import {
    createLangChainModel,
    convertMessagesToLangChain,
    getAIMetricsFromResponse,
    LangChainRunnerFactory,
  } from '@launchdarkly/server-sdk-ai-langchain';
  import { createReactAgent } from '@langchain/langgraph/prebuilt';
  import { MemorySaver } from '@langchain/langgraph';

  const sdkKey = process.env.LAUNCHDARKLY_SDK_KEY;
  const CONFIG_KEY = 'langgraph-agent';

  const ldClient: LDClient = init(sdkKey);

  try {
    await ldClient.waitForInitialization({ timeout: 10 });
  } catch {
    console.error('LaunchDarkly SDK failed to initialize');
    return;
  }

  const aiClient: LDAIClient = initAi(ldClient);

  const context: LDContext = { kind: 'user', key: 'user-123', name: 'Sandy' };

  // Pass a default for improved resiliency when the AgentControl config is unavailable
  // or LaunchDarkly is unreachable; omit for a disabled default.
  // Example:
  //   const fallback = {
  //     enabled: true,
  //     model: { name: 'gpt-5' },
  //     provider: { name: 'openai' },
  //     instructions: 'You are a helpful assistant.',
  //   };
  //   const agentConfig = await aiClient.agentConfig(CONFIG_KEY, context, fallback);
  const agentConfig: LDAIAgentConfig = await aiClient.agentConfig(CONFIG_KEY, context);

  if (!agentConfig.enabled || !agentConfig.instructions) {
    console.log('Agent Config is disabled — run the Python notebook first');
    await ldClient.close();
    return;
  }

  // createLangChainModel reads agentConfig.model.name / .parameters
  // and picks the right chat model class (OpenAI, Anthropic, …) with no per-provider branching.
  const llm = await createLangChainModel(agentConfig);

  // Resolve the agent's tool list from the active variation. LaunchDarkly returns
  // attached tools under `parameters.tools` in OpenAI function shape; we only need
  // the names to look up the local handlers from TOOL_REGISTRY.
  const ldToolParams = (agentConfig.model?.parameters as any)?.tools ?? [];
  const resolvedTools = ldToolParams
    .map((t: any) => TOOL_REGISTRY[t.name])
    .filter(Boolean);

  // MemorySaver gives the ReAct agent short-term memory per thread_id —
  // follow-up turns on the same thread see the earlier conversation.
  const checkpointer = new MemorySaver();
  const agent = createReactAgent({
    llm,
    tools: resolvedTools,
    prompt: agentConfig.instructions,
    checkpointer,
  });
  ```
</CodeGroup>

Invoke the agent and track metrics. Each turn is one **execution** as far as the tracker is concerned: `track_metrics_of_async` (Python) / `trackMetricsOf` (Node.js) records duration and tracks success or error itself, so the surrounding `try`/`except` only needs to log. The Python example uses the SDK's `sum_token_usage_from_messages` helper to aggregate token counts and `get_tool_calls_from_response` to feed `track_tool_call`. The Node.js example uses `LangChainProvider.getAIMetricsFromResponse` per message and reads `msg.tool_calls` directly until the matching JS helper ships.

<CodeGroup>
  ```python title="Python" expandable lines wrap theme={null}
  from ldai.providers.types import LDAIMetrics
  from ldai_langchain import get_tool_calls_from_response, sum_token_usage_from_messages


  async def run_turn(agent, agent_config, user_input, thread_id):
      # Each tracker is one execution. `track_metrics_of_async`
      # records duration + success/error itself; the extractor only returns
      # LDAIMetrics. The SDK helpers handle token aggregation and tool-call
      # name extraction.
      tracker = agent_config.create_tracker()
      try:
          result = await tracker.track_metrics_of_async(
              lambda: agent.ainvoke(
                  {"messages": [{"role": "user", "content": user_input}]},
                  config={"configurable": {"thread_id": thread_id}},
              ),
              lambda res: LDAIMetrics(
                  success=True,
                  usage=sum_token_usage_from_messages(res.get("messages", [])),
              ),
          )
          for msg in result.get("messages", []):
              for name in get_tool_calls_from_response(msg):
                  tracker.track_tool_call(name)
          messages = result.get("messages", [])
          if messages:
              print(f"Agent: {messages[-1].content}")
      except Exception as e:
          # `track_metrics_of_async` already recorded the error and re-raised.
          print(f"Error: {e}")


  thread_id = "demo-thread"
  await run_turn(agent, agent_config, "What's the status of order ORD-123?", thread_id)
  await run_turn(agent, agent_config, "What about ORD-456?", thread_id)
  await run_turn(agent, agent_config, "Summarize both orders for me.", thread_id)
  ```

  ```typescript title="Node.js" expandable lines wrap theme={null}
  // Aggregate token usage across every message with the SDK helper.
  function langgraphMetrics(result: any) {
    let input = 0;
    let output = 0;
    let total = 0;
    for (const msg of result.messages || []) {
      const m = LangChainProvider.getAIMetricsFromResponse(msg);
      if (m.usage) {
        input += m.usage.input;
        output += m.usage.output;
        total += m.usage.total;
      }
    }
    return {
      success: true,
      usage: total > 0 ? { total, input, output } : undefined,
    };
  }

  async function runTurn(input: string, threadId: string): Promise<void> {
    // Each tracker is one execution. trackMetricsOf wraps the call,
    // tracks duration, and tracks success/error itself — no manual catch handling.
    const tracker = agentConfig.createTracker!();
    try {
      const result = await tracker.trackMetricsOf(
        langgraphMetrics,
        () =>
          (agent as any).invoke(
            { messages: [{ role: 'user', content: input }] },
            { configurable: { thread_id: threadId } },
          ),
      );
      // Tool-call telemetry: walk the result messages. (When the JS SDK adds
      // `LangChainProvider.getToolCallsFromResponse`, this collapses to one helper call.)
      for (const msg of result.messages || []) {
        for (const tc of msg.tool_calls || []) {
          tracker.trackToolCall(tc.name);
        }
      }
      const messages = result.messages || [];
      if (messages.length > 0) {
        const content = messages[messages.length - 1].content;
        console.log(`Agent: ${typeof content === 'string' ? content : JSON.stringify(content)}`);
      }
    } catch (error) {
      console.error(`Error: ${error}`);
    }
  }

  const threadId = 'demo-thread';
  await runTurn("What's the status of order ORD-123?", threadId);
  await runTurn('What about ORD-456?', threadId);
  await runTurn('Summarize both orders for me.', threadId);
  ```
</CodeGroup>

The fallback argument to `agent_config` / `agentConfig` is optional. When omitted, LaunchDarkly returns a disabled config if the flag is off or the SDK is unreachable. Pass an explicit fallback to keep the agent running during outages.

## Complete example

Here is a complete working example that combines all the steps.

<Accordion title="Click to expand full example code">
  <CodeGroup>
    ```python title="Python" expandable lines wrap theme={null}
    import asyncio
    import os
    import ldclient
    from ldclient import Context
    from ldclient.config import Config
    from ldai.client import LDAIClient
    from ldai.providers.types import LDAIMetrics
    from ldai_langchain import (
        create_langchain_model,
        get_tool_calls_from_response,
        sum_token_usage_from_messages,
    )
    from langchain_core.tools import tool
    from langchain.agents import create_agent
    from langgraph.checkpoint.memory import MemorySaver
    from dotenv import load_dotenv

    load_dotenv()

    SDK_KEY = os.environ.get("LAUNCHDARKLY_SDK_KEY")
    AGENT_CONFIG_KEY = "langgraph-agent"


    @tool
    def get_order_status(order_id: str) -> str:
        """Look up the status of a customer order by order ID."""
        orders = {
            "ORD-123": "Shipped — arrives Thursday",
            "ORD-456": "Processing — estimated ship date: tomorrow",
            "ORD-789": "Delivered on Monday",
        }
        return orders.get(order_id, f"No order found with ID {order_id}")


    # Map tool keys (matching the LaunchDarkly tool keys) to local handlers.
    TOOL_REGISTRY = {"get_order_status": get_order_status}


    async def run_turn(agent, agent_config, user_input, thread_id):
        # Each tracker is one execution. `track_metrics_of_async`
        # records duration + success/error itself; the extractor only returns
        # LDAIMetrics. SDK helpers handle token aggregation and tool-call name
        # extraction.
        tracker = agent_config.create_tracker()
        try:
            result = await tracker.track_metrics_of_async(
                lambda: agent.ainvoke(
                    {"messages": [{"role": "user", "content": user_input}]},
                    config={"configurable": {"thread_id": thread_id}},
                ),
                lambda res: LDAIMetrics(
                    success=True,
                    usage=sum_token_usage_from_messages(res.get("messages", [])),
                ),
            )
            for msg in result.get("messages", []):
                for name in get_tool_calls_from_response(msg):
                    tracker.track_tool_call(name)
            messages = result.get("messages", [])
            if messages:
                print(f"Agent: {messages[-1].content}")
        except Exception as e:
            # `track_metrics_of_async` already recorded the error and re-raised.
            print(f"Error: {e}")


    async def async_main():
        ldclient.set_config(Config(SDK_KEY))
        if not ldclient.get().is_initialized():
            print("LaunchDarkly SDK failed to initialize")
            return

        ai_client = LDAIClient(ldclient.get())

        context = Context.builder("user-123").kind("user").name("Sandy").build()

        # Pass a default for improved resiliency when the AgentControl config is unavailable
        # or LaunchDarkly is unreachable; omit for a disabled default.
        # Example:
        #   from ldai import AIAgentConfigDefault
        #   default = AIAgentConfigDefault(
        #       enabled=True,
        #       model={"name": "gpt-5"},
        #       provider={"name": "openai"},
        #       instructions="You are a helpful assistant.",
        #   )
        #   agent_config = ai_client.agent_config(AGENT_CONFIG_KEY, context, default)
        agent_config = ai_client.agent_config(AGENT_CONFIG_KEY, context)

        if not agent_config.enabled:
            print("Agent Config is disabled — run the notebook to set up the config")
            return

        # create_langchain_model reads agent_config.model.name / .parameters and picks the
        # right chat model class (OpenAI, Anthropic, …) with no per-provider branching.
        llm = create_langchain_model(agent_config)

        # Resolve the agent's tool list from the active variation. LaunchDarkly returns
        # attached tools under `parameters.tools` in OpenAI function shape.
        ld_tool_params = (agent_config.model.to_dict().get("parameters") or {}).get("tools") or []
        resolved_tools = [
            TOOL_REGISTRY[t["name"]] for t in ld_tool_params if t["name"] in TOOL_REGISTRY
        ]

        # MemorySaver gives the ReAct agent short-term memory per thread_id —
        # follow-up turns on the same thread see the earlier conversation.
        checkpointer = MemorySaver()
        agent = create_agent(
            llm,
            resolved_tools,
            system_prompt=agent_config.instructions,
            checkpointer=checkpointer,
        )

        # Three turns on one thread: first fires the tool, second reuses memory
        # to answer a follow-up that reuses the tool, third summarizes — no tool.
        # Each turn gets its own tracker (its own runId).
        thread_id = "demo-thread"
        await run_turn(agent, agent_config, "What's the status of order ORD-123?", thread_id)
        await run_turn(agent, agent_config, "What about ORD-456?", thread_id)
        await run_turn(agent, agent_config, "Summarize both orders for me.", thread_id)

        # Always flush events before closing — trailing events are at risk of being
        # lost otherwise, in short-lived scripts and long-running services alike.
        ldclient.get().flush()
        ldclient.get().close()


    def main():
        asyncio.run(async_main())


    if __name__ == "__main__":
        main()
    ```

    ```typescript title="Node.js" expandable lines wrap theme={null}
    import dotenv from 'dotenv';
    dotenv.config();
    import { init, LDClient, LDContext } from '@launchdarkly/node-server-sdk';
    import { initAi, type LDAIClient, type LDAIAgentConfig } from '@launchdarkly/server-sdk-ai';
    import {
      createLangChainModel,
      convertMessagesToLangChain,
      getAIMetricsFromResponse,
      LangChainRunnerFactory,
    } from '@launchdarkly/server-sdk-ai-langchain';
    import { createReactAgent } from '@langchain/langgraph/prebuilt';
    import { MemorySaver } from '@langchain/langgraph';
    import { tool } from '@langchain/core/tools';
    import { z } from 'zod';

    const SDK_KEY = process.env.LAUNCHDARKLY_SDK_KEY;
    const CONFIG_KEY = 'langgraph-agent';

    // tool() types from @langchain/core are deep; cast to any to avoid TS2589
    const getOrderStatus = (tool as any)(
      async ({ order_id }: { order_id: string }) => {
        const orders: Record<string, string> = {
          'ORD-123': 'Shipped — arrives Thursday',
          'ORD-456': 'Processing — estimated ship date: tomorrow',
          'ORD-789': 'Delivered on Monday',
        };
        return orders[order_id] || `No order found with ID ${order_id}`;
      },
      {
        name: 'get_order_status',
        description: 'Look up the status of a customer order by order ID',
        schema: z.object({
          order_id: z.string().describe('The order ID to look up'),
        }),
      },
    );

    // Map tool keys (matching the LaunchDarkly tool keys) to local handlers.
    const TOOL_REGISTRY: Record<string, any> = { get_order_status: getOrderStatus };

    // Aggregate token usage across every message with the SDK helper.
    function langgraphMetrics(result: any) {
      let input = 0;
      let output = 0;
      let total = 0;
      for (const msg of result.messages || []) {
        const m = LangChainProvider.getAIMetricsFromResponse(msg);
        if (m.usage) {
          input += m.usage.input;
          output += m.usage.output;
          total += m.usage.total;
        }
      }
      return {
        success: true,
        usage: total > 0 ? { total, input, output } : undefined,
      };
    }

    async function main() {
      if (!SDK_KEY) {
        console.error('LAUNCHDARKLY_SDK_KEY not set');
        return;
      }

      const ldClient: LDClient = init(SDK_KEY);

      try {
        await ldClient.waitForInitialization({ timeout: 10 });
      } catch {
        console.error('LaunchDarkly SDK failed to initialize');
        return;
      }

      const aiClient: LDAIClient = initAi(ldClient);

      const context: LDContext = { kind: 'user', key: 'user-123', name: 'Sandy' };

      // Pass a default for improved resiliency when the AgentControl config is unavailable
      // or LaunchDarkly is unreachable; omit for a disabled default.
      // Example:
      //   const fallback = {
      //     enabled: true,
      //     model: { name: 'gpt-5' },
      //     provider: { name: 'openai' },
      //     instructions: 'You are a helpful assistant.',
      //   };
      //   const agentConfig = await aiClient.agentConfig(CONFIG_KEY, context, fallback);
      const agentConfig: LDAIAgentConfig = await aiClient.agentConfig(CONFIG_KEY, context);

      if (!agentConfig.enabled || !agentConfig.instructions) {
        console.log('Agent Config is disabled — run the Python notebook first');
        await ldClient.close();
        return;
      }

      // createLangChainModel reads agentConfig.model.name / .parameters
      // and picks the right chat model class (OpenAI, Anthropic, …) with no per-provider branching.
      const llm = await createLangChainModel(agentConfig);

      // Resolve the agent's tool list from the active variation. LaunchDarkly returns
      // attached tools under `parameters.tools` in OpenAI function shape.
      const ldToolParams = (agentConfig.model?.parameters as any)?.tools ?? [];
      const resolvedTools = ldToolParams
        .map((t: any) => TOOL_REGISTRY[t.name])
        .filter(Boolean);

      // MemorySaver gives the ReAct agent short-term memory per thread_id —
      // follow-up turns on the same thread see the earlier conversation.
      const checkpointer = new MemorySaver();
      const agent = createReactAgent({
        llm,
        tools: resolvedTools,
        prompt: agentConfig.instructions,
        checkpointer,
      });

      async function runTurn(input: string, threadId: string): Promise<void> {
        // Each tracker is one execution. trackMetricsOf wraps the call,
        // tracks duration, and tracks success/error itself — no manual catch handling.
        const tracker = agentConfig.createTracker!();
        try {
          const result = await tracker.trackMetricsOf(
            langgraphMetrics,
            () =>
              (agent as any).invoke(
                { messages: [{ role: 'user', content: input }] },
                { configurable: { thread_id: threadId } },
              ),
          );
          // Tool-call telemetry: walk the result messages. (When the JavaScript SDK
          // adds `getToolCallsFromResponse`, this collapses to one helper call.)
          for (const msg of result.messages || []) {
            for (const tc of msg.tool_calls || []) {
              tracker.trackToolCall(tc.name);
            }
          }
          const messages = result.messages || [];
          if (messages.length > 0) {
            const content = messages[messages.length - 1].content;
            console.log(`Agent: ${typeof content === 'string' ? content : JSON.stringify(content)}`);
          }
        } catch (error) {
          console.error(`Error: ${error}`);
        }
      }

      const threadId = 'demo-thread';
      await runTurn("What's the status of order ORD-123?", threadId);
      await runTurn('What about ORD-456?', threadId);
      await runTurn('Summarize both orders for me.', threadId);

      // Always flush events before closing — trailing events are at risk of being
      // lost otherwise, in short-lived scripts and long-running services alike.
      await ldClient.flush();
      await ldClient.close();
    }

    main().catch((err) => {
      console.error('Fatal error:', err);
      process.exit(1);
    });
    ```
  </CodeGroup>
</Accordion>

## Step 5: Monitor results

View metrics for your AgentControl config in the LaunchDarkly UI.

To monitor results:

1. In LaunchDarkly, navigate to your AgentControl config.
2. Select the **Monitoring** tab.

LaunchDarkly displays metrics including:

* Generation count
* Token usage (input, output, total)
* Time to generate
* Error rate

Use these metrics to compare agent performance, identify cost differences, and make data-driven decisions about which configuration to use for different user segments. To learn more, read [Monitor AgentControl configs](/docs/home/agentcontrol/monitor).

To view aggregated metrics across all your AgentControl configs, navigate to **Insights** in the left sidebar under the **AI** section. The Insights overview page displays cost, latency, error rate, invocation counts, and model distribution across your organization. To learn more, read about [AI insights](/docs/home/agentcontrol/insights).

<Frame caption="The Insights overview page showing cost, latency, error rate, and invocation metrics for a LangGraph AgentControl config.">
  <img src="https://mintcdn.com/launchdarkly/Y_qcqLWSC5ccm6eB/images/__LD_UI_no_test/guide-ai-config-langgraph-insights.png?fit=max&auto=format&n=Y_qcqLWSC5ccm6eB&q=85&s=b717b4b10aa48f0421ee5f1c42a4ef21" alt="The Insights overview page showing cost, latency, error rate, and invocation metrics for a LangGraph AgentControl config." width="2801" height="1554" data-path="images/__LD_UI_no_test/guide-ai-config-langgraph-insights.png" />
</Frame>

## Agent mode vs completion mode

| Aspect | Agent Mode | Completion Mode |
| - | - | - |
| Config field | `instructions` (string) | `messages` (array) |
| SDK method | `agent_config()` | `completion_config()` |
| Default class | `AIAgentConfigDefault` | `AICompletionConfigDefault` |
| Use case | Multi-step workflows, tool use | Single-turn completions |

## Conclusion

In this guide, you learned how to integrate LangGraph agent workflows with LaunchDarkly AgentControl to manage agent configuration outside of your application code.

You can now:

* Change agent models and instructions without redeploying your application
* Target different agent configurations to different users based on context attributes
* Track and compare agent performance across variations
* Maintain conversation state with LangGraph checkpointing
* Coordinate multi-agent workflows with centralized configuration

To explore additional capabilities, read:

* [Run experiments with AgentControl](/docs/home/agentcontrol/experimentation) to compare agent variations using statistical analysis
* [Config targeting](/docs/home/agentcontrol/target) to serve different agents to different user segments
* [Agent graphs](/docs/home/agentcontrol/agent-graphs) to orchestrate multi-agent workflows

For more AgentControl examples, read the other [AgentControl guides](/docs/guides/agentcontrol) in this section.

<View title="Developer">
  <Note>
    **Want to know more? Start a trial.**

    Your 14-day trial begins as soon as you sign up. Get started in minutes using the in-app Quickstart. You'll discover how easy it is to release, monitor, and optimize your software.<br /><br />

    Want to try it out? <a href="https://app.launchdarkly.com/signup">Start a trial</a>.
  </Note>
</View>

<View title="Federal docs" />

<View title="EU docs" />
