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

# Build a custom frontend for a Managed Deep Agent

> Call a Managed Deep Agents deployment from your own browser or server application instead of reaching the agent through a channel or LangSmith Studio.

A custom frontend is any application you write that calls a deployment directly instead of reaching the agent through a [channel](/langsmith/python/managed-deep-agents-channels.md) or [LangSmith Studio](/langsmith/python/managed-deep-agents-local-development.md#start-local-studio). Use one when the agent belongs inside a product surface you control, such as a support console, an internal dashboard, or a customer-facing chat page.

Managed Deep Agents deployments run on [LangSmith Agent Server](/langsmith/agent-server-overview.md). A frontend talks to the same threads, runs, and streaming endpoints that every Agent Server client uses.

> [!NOTE]
>
> Managed Deep Agents is in **public [beta](/langsmith/release-stages.md)** on [LangSmith Cloud](/langsmith/cloud.md).

## Choose an identity provider

Every request to a deployment names a caller, and the frontend supplies that name. Which [identity provider](/langsmith/python/managed-deep-agents-identity.md) fits depends on where the frontend code runs, not on the language it is written in.

| Where the frontend calls the deployment from | Identity provider |
| - | - |
| The user's browser | An external auth provider. The browser sends an access token and the deployment verifies it. See [Supabase](/langsmith/python/managed-deep-agents-identity.md#configure-identity-with-supabase). |
| Your own server, which already authenticates the user | [Your own backend](/langsmith/python/managed-deep-agents-identity.md#configure-identity-with-your-own-backend). Your server asserts the user ID behind a shared ingress secret. |
| Your own server, with no end-user accounts of its own | The [LangSmith API key](/langsmith/python/managed-deep-agents-identity.md#configure-identity-with-a-langsmith-api-key) default. The key itself is the caller. |

Either server row fits a server-rendered application in any language. Streamlit, Gradio, and Dash run their code on the server, as do a Next.js route handler and an Express proxy. A frontend pointed at [`mda dev`](/langsmith/python/managed-deep-agents-local-development#understand-local-behavior) works before you configure a provider, because the local server supplies a test user.

> [!WARNING]
>
> A LangSmith API key reaches every resource in the workspace, not only this deployment, so it must never ship in browser JavaScript. Use an external auth provider when the browser calls the deployment directly, and backend identity when your own server calls it on the user's behalf.

## Connect to the deployment

A frontend needs two values.

**Deployment API URL.** `mda deploy` prints the LangSmith deployment dashboard URL, not the API URL. Open that dashboard, then copy the **API URL** from the deployment details view. The local server started by [`mda dev`](/langsmith/python/managed-deep-agents-local-development) serves the same routes at the URL the CLI prints.

**Assistant ID.** Clients pass the agent's `name` from the [agent definition](/langsmith/python/managed-deep-agents-agent-definition.md#parameters). Managed Deep Agents also uses that name as the graph ID:

```python agent.py theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
from managed_deepagents import define_deep_agent

agent = define_deep_agent(
    name="research-assistant",  # [!code highlight]
    model="openai:gpt-5.5",
)
```

Clients address an agent named `research-assistant` with that same string. Renaming the agent changes the value clients send, so treat the name as part of the deployment's public interface.

## Build a web frontend

The [LangChain frontend SDKs](/oss/python/langchain/frontend/overview.md) connect a browser application to an Agent Server deployment. `useStream` exposes messages, tool calls, interrupts, and thread state as reactive values:

| Framework | Package | Entry point |
| - | - | - |
| React | `@langchain/react` | `useStream` |
| Vue | `@langchain/vue` | `useStream` |
| Svelte | `@langchain/svelte` | `useStream` |
| Angular | `@langchain/angular` | `injectStream` |

The frontend runs in the browser, so these examples are TypeScript even when the agent is written in Python.

Under an external auth provider, the browser sends the access token from the signed-in session. That token rotates, so re-read it whenever the session changes rather than capturing it once at mount:

```ts useAuthHeaders.ts theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { useEffect, useState } from "react";

import { supabase } from "./supabase";

export function useAuthHeaders() {
  const [headers, setHeaders] = useState<Record<string, string>>();

  useEffect(() => {
    const refresh = async () => {
      const { data } = await supabase.auth.getSession();
      const token = data.session?.access_token;
      setHeaders(token ? { Authorization: `Bearer ${token}` } : undefined);
    };

    void refresh();
    const { data } = supabase.auth.onAuthStateChange(() => void refresh()); // [!code highlight]
    return () => data.subscription.unsubscribe();
  }, []);

  return headers;
}
```

The highlighted subscription fires on sign-in, sign-out, and token refresh. Without it the hook holds the token captured at mount, and requests start failing with 401 once that token expires.

Point the stream at the deployment API URL, the agent name, and those headers:

```tsx Chat.tsx theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import { useStream } from "@langchain/react";
import { useState } from "react";

import { useAuthHeaders } from "./useAuthHeaders";

export function Chat() {
  const [input, setInput] = useState("");
  const headers = useAuthHeaders();
  const stream = useStream({
    apiUrl: import.meta.env.VITE_DEPLOYMENT_API_URL,
    assistantId: "research-assistant",
    defaultHeaders: headers,
  });

  return (
    <main>
      {stream.messages.map((message) => (
        <p key={message.id}>{message.text}</p>
      ))}

      <form
        onSubmit={(event) => {
          event.preventDefault();
          stream.submit({ messages: [{ type: "human", content: input }] });
          setInput("");
        }}
      >
        <input value={input} onChange={(event) => setInput(event.target.value)} />
        <button type="submit" disabled={stream.isLoading}>
          Send
        </button>
      </form>
    </main>
  );
}
```

Under backend identity the browser never reaches the deployment. Point `apiUrl` at your own server and forward the Agent Server routes from there, stamping `x-mda-ingress-secret` and the user ID your auth layer resolved on each upstream request. For the headers and the shared secret, see [Configure identity with your own backend](/langsmith/python/managed-deep-agents-identity.md#configure-identity-with-your-own-backend).

The same frontend patterns work against a Managed Deep Agents deployment. See [tool-call rendering](/oss/python/langchain/frontend/tool-calling.md), [generative UI](/oss/python/langchain/frontend/generative-ui-overview.md), [human-in-the-loop approval](/oss/python/langchain/frontend/human-in-the-loop.md), [queued submissions](/oss/python/langchain/frontend/message-queues.md), and [durable rejoin](/oss/python/langchain/frontend/join-rejoin.md).

### Handle authorization interrupts

When the agent needs a [user-owned connection](/langsmith/python/managed-deep-agents-connections.md) the caller has not granted, the run pauses and `stream.interrupt` carries a credential gate payload. A browser frontend renders the consent links and resumes with `stream.respond`. For the payload shape and a worked example, see [Handle the authorization interrupt](/langsmith/python/managed-deep-agents-connections.md#handle-the-authorization-interrupt).

## Build a Python frontend

Streamlit, Gradio, and Dash render on the server, so no credential reaches the browser and the API-key default works without extra setup. All three use the same [LangGraph SDK](https://reference.langchain.com/python/langgraph-sdk) client, so the connection code below applies to any of them.

Install the SDK alongside your application framework:

```bash theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
uv add langgraph-sdk streamlit
```

Create a client, then stream a run against a thread:

```python theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
from langgraph_sdk import get_sync_client

client = get_sync_client(
    url=DEPLOYMENT_API_URL,
    api_key=LANGSMITH_API_KEY,
)

thread = client.threads.create()

for chunk in client.runs.stream(
    thread["thread_id"],
    "research-assistant",
    input={"messages": [{"role": "user", "content": "Summarize this week's incidents."}]},
    stream_mode="messages",
):
    ...
```

Three details govern how that loop behaves:

* **`api_key`**: Sets the `x-api-key` header. Passing `headers={"x-api-key": ...}` instead raises `ValueError: Cannot set reserved header 'x-api-key'`.
* **Environment fallback**: When `api_key` is omitted, the client tries `LANGGRAPH_API_KEY`, then `LANGSMITH_API_KEY`, then `LANGCHAIN_API_KEY`. For Supabase or backend identity, pass `api_key=None` explicitly and put the token in `headers`. Otherwise a stray environment variable attaches a second credential.
* **`messages/partial`**: Carries the whole message so far, not a delta. Replace the rendered text on each event rather than appending, or the output repeats itself.

### Stream into a Streamlit chat

This application keeps one thread per browser session and rewrites the assistant bubble as each event arrives:

```python app.py theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
import os

import streamlit as st
from langgraph_sdk import get_sync_client

ASSISTANT_ID = "research-assistant"


@st.cache_resource
def get_agent_client():
    return get_sync_client(
        url=os.environ["DEPLOYMENT_API_URL"],
        api_key=os.environ["LANGSMITH_API_KEY"],
    )


def text_of(message):
    return "".join(
        block.get("text", "")
        for block in message.get("content", [])
        if block.get("type") == "text"
    )


client = get_agent_client()

if "thread_id" not in st.session_state:
    st.session_state.thread_id = client.threads.create()["thread_id"]
    st.session_state.history = []

for role, text in st.session_state.history:
    st.chat_message(role).markdown(text)

if prompt := st.chat_input("Message the agent"):
    st.session_state.history.append(("user", prompt))
    st.chat_message("user").markdown(prompt)

    with st.chat_message("assistant"):
        placeholder = st.empty()
        answer = ""
        for chunk in client.runs.stream(
            st.session_state.thread_id,
            ASSISTANT_ID,
            input={"messages": [{"role": "user", "content": prompt}]},
            stream_mode="messages",
        ):
            if chunk.event != "messages/partial":
                continue
            for message in chunk.data:
                if message.get("type") == "ai":
                    answer = text_of(message)
                    placeholder.markdown(answer)  # [!code highlight]

    st.session_state.history.append(("assistant", answer))
```

The highlighted line replaces the bubble contents on every event. Appending there produces duplicated text, because each event repeats the message from the beginning.

`text_of` reads content blocks rather than a plain string. Agent messages arrive as a list of typed blocks, so a frontend that expects `message["content"]` to be text renders nothing. The web SDKs deserialize each event into a message object that exposes a `text` accessor, which is why the React example above reads `message.text`. The LangGraph SDK yields plain dictionaries, so a Python frontend extracts the text itself.

Gradio and Dash use the same client and the same loop. Substitute their streaming primitive for the Streamlit placeholder: a generator function for `gr.ChatInterface`, or a `dcc.Interval` callback for Dash.

## See also

* [Identity](/langsmith/python/managed-deep-agents-identity.md): configure how callers authenticate to the deployment.
* [Frontend overview](/oss/python/langchain/frontend/overview.md): message rendering, tool calls, interrupts, and generative UI patterns.
* [Connections](/langsmith/python/managed-deep-agents-connections.md): handle the credential authorization interrupt in your UI.
* [MCP endpoint](/langsmith/python/managed-deep-agents-mcp-endpoint.md): expose the agent to MCP clients instead of calling the API.
* [Channels](/langsmith/python/managed-deep-agents-channels.md): reach the agent from Slack or your own HTTP service.

***

[Connect these docs](/use-these-docs.md) to your agent of choice via MCP for real-time answers.

[Edit this page on GitHub](https://github.com/langchain-ai/docs/edit/main/src/langsmith/managed-deep-agents-custom-frontend.mdx) or [file an issue](https://github.com/langchain-ai/docs/issues/new/choose).
