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

# Migrating from LangChain

> Step-by-step guide for moving a raw LangChain agent onto SwarmD

# Migrating from LangChain

This guide walks through migrating an existing raw LangChain agent
onto SwarmD using the `swarmd-langchain` package. By the end your
agent's `main.py` contains only domain logic — the tools, the prompt,
and the model choice — and `swarmd-langchain` owns every platform
concern.

If you have not read [LangChain on SwarmD](./langchain) yet, do that
first. It explains what each of `create_runtime`, `create_llm_agent`,
and `serve` actually does. This guide focuses on the *migration
journey* — what to delete, what to replace, and how to roll it out —
rather than re-explaining the helpers.

Each step shows the **before** (raw LangChain) and the **after**
(SwarmD) side by side. The shape mirrors
[Migrating from Google ADK](./migration-google-adk) on purpose — the
helper trio is the same.

## What you are migrating away from

A typical standalone LangChain agent — one file owning everything: the
LangChain tool functions, hand-rolled HTTP tools that POST to other
agents directly, the LLM, and (if it serves over A2A at all) a custom
Starlette adapter.

```python main.py theme={null}
import os

import httpx
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

load_dotenv()


@tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"The weather in {city} is sunny"


# Every remote agent is a hand-rolled HTTP tool — URL baked into source
@tool
def call_time_agent(prompt: str) -> str:
    """Ask the time agent for time information."""
    r = httpx.post(
        "https://time-agent.internal:8080/messages",
        json={"prompt": prompt},
        timeout=60,
    )
    return r.text


@tool
def call_calendar_agent(prompt: str) -> str:
    """Ask the calendar agent to manage events."""
    r = httpx.post(
        "https://calendar-agent.internal:8080/messages",
        json={"prompt": prompt},
        timeout=60,
    )
    return r.text


agent = create_agent(
    model=ChatOpenAI(model="gpt-4o", api_key=os.environ["OPENAI_API_KEY"]),
    tools=[get_weather, call_time_agent, call_calendar_agent],
    system_prompt="You are a helpful weather agent.",
)

# ...plus whatever Starlette / FastAPI you wrote to expose this over HTTP
```

Pain points that compound the moment this leaves your laptop:

* Sub-agent URLs and request shapes are baked into source. Each
  `call_*_agent` tool reinvents request/response, error handling, and
  retries.
* No A2A protocol. Your "tools" call arbitrary endpoints — the
  downstream might be A2A, REST, or something else, and your code has
  to know.
* No polling for long-running tasks. If the downstream returns a
  `working` task, your tool either blocks forever or returns a
  useless intermediate payload.
* No identity between agents.
* No central audit, policy, or HITL — the relay isn't in the path.
* MCP tools aren't there at all.

The goal of the migration is to keep the tool function
(`get_weather`) and the agent's identity (system prompt, model)
exactly as they are while deleting every `call_*_agent` tool and the
bespoke Starlette / FastAPI wrapper.

## What you are migrating to

```python main.py theme={null}
from dotenv import load_dotenv
load_dotenv()

from langchain_core.tools import tool

from swarmd_langchain import create_llm_agent, create_runtime, serve


@tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"The weather in {city} is sunny"


runtime = create_runtime()
agent = create_llm_agent(
    runtime,
    name="weather_agent",
    description="A weather agent that collaborates with other agents",
    instruction="You are a helpful weather agent.",
    tools=[get_weather],
)

if __name__ == "__main__":
    serve(agent, runtime)
```

That's the whole file. For what each helper does under the hood, see
[LangChain on SwarmD](./langchain).

## Migration steps

### Step 1 — Register the agent

In the [dashboard](https://app.swarmd.ai) (or via
`POST /registry/v1/agents`), register `weather_agent`. Capture the
three secrets shown once in the response: `agentId`, `clientSecret`,
and `webhookSecret`. Store them with your deployment configuration.

**After this step:** the platform knows your agent exists. You have
the OAuth2 credentials it will use for outbound calls.

### Step 2 — Install the SDK

```bash theme={null}
pip install swarmd-langchain
```

You can keep your existing LangChain pins — `swarmd-langchain`
declares compatible ranges, not exact versions.

**After this step:** `from swarmd_langchain import create_runtime,
create_llm_agent, serve` resolves.

### Step 3 — Add platform credentials to `.env`

Append to your existing `.env`:

```bash .env theme={null}
SWARMD_AGENT_ID=00000000-0000-0000-0000-000000000000
SWARMD_CLIENT_SECRET=...
SWARMD_WEBHOOK_SECRET=...
SWARMD_BASE_URL=https://api.swarmd.ai
SWARMD_TOKEN_URL=https://auth.swarmd.ai/realms/swarmd/protocol/openid-connect/token
```

**After this step:** with these vars set, `create_runtime()` will
attach the agent to SwarmD. With them unset, the agent still runs
locally as a plain LangChain agent against direct OpenAI — useful for
local dev without a SwarmD account.

### Step 4 — Replace bootstrap with `create_runtime()`

```python theme={null}
# Before — load .env, then everything below is hand-wired
load_dotenv()

# After
load_dotenv()
from swarmd_langchain import create_runtime
runtime = create_runtime()
```

**After this step:** the runtime owns auth, token caching, registry
calls, MCP discovery, and correlation-ID propagation. You hold one
reference to `runtime` and pass it into the next two helpers.

### Step 5 — Delete the hand-rolled HTTP tools

```python theme={null}
# Before — each remote agent is its own @tool with hardcoded URL
@tool
def call_time_agent(prompt: str) -> str:
    ...
@tool
def call_calendar_agent(prompt: str) -> str:
    ...

tools = [get_weather, call_time_agent, call_calendar_agent]

# After — keep local tools only; remote agents arrive via discovery
tools = [get_weather]
```

You don't import `httpx`, you don't write retry logic, and you don't
type the downstream URL anywhere in source.

**After this step:** half the surface area of `main.py` disappears.
This is the highest-leverage change in the migration.

### Step 6 — Replace `create_agent(...)` with `create_llm_agent(runtime, ...)`

```python theme={null}
# Before
agent = create_agent(
    model=ChatOpenAI(model="gpt-4o", api_key=os.environ["OPENAI_API_KEY"]),
    tools=[get_weather, call_time_agent, call_calendar_agent],   # mixed local + remote
    system_prompt="You are a helpful weather agent.",
)

# After
agent = create_llm_agent(
    runtime,
    name="weather_agent",
    description="A weather agent that collaborates with other agents",
    instruction="You are a helpful weather agent.",
    tools=[get_weather],   # local tools only — sub-agents + MCP discovered
)
```

Your system prompt becomes `instruction`, and the agent gets a stable
`name` and `description` — these show up on its A2A card, in audit
logs, and in the dashboard.

For exactly what `create_llm_agent` does internally, see
[the helper reference](./langchain#create_llm_agent_runtime_name_description_instruction_tools).

**After this step:** the returned `CompiledStateGraph` is a regular
LangGraph with local tools + remote sub-agent tools + MCP tools
already wired in. You can still call `agent.ainvoke({"messages": [...]})` against it exactly as before.

### Step 7 — Replace your A2A wrapper with `serve(agent, runtime)`

If you previously hand-rolled a Starlette / FastAPI server to expose
the LangChain agent over HTTP, delete it. If you weren't exposing the
agent over HTTP at all (you were calling `agent.ainvoke(...)` from
another process), `serve` is what makes the migration's value land —
it gives your agent the same A2A surface every SwarmD agent has, so
the relay can route to it like any other.

```python theme={null}
# Before — bespoke FastAPI / Starlette wrapper, or no wrapper at all

# After
serve(agent, runtime)
```

For exactly what `serve` mounts (A2A executor, agent card, `/admin`,
correlation middleware, task store, refresh-in-place), see
[the helper reference](./langchain#serve_agent_runtime_skills_none_on_refresh_none).

**After this step:** the agent process exposes the A2A protocol on
`HOST:PORT`, plus the `/admin` surface SwarmD uses to push refresh
events.

### Step 8 — Subscribe to downstream agents and MCP servers

Configuration only — no code change. In the dashboard subscribe
`weather_agent` to the sub-agents and MCP servers it needs (the same
ones you used to hardcode in Step 5). The dashboard's
`POST /registry/v1/agents/{id}/subscriptions` endpoint works too.

**After this step:** the platform fires a `SUBSCRIPTION_CHANGED`
webhook. Your running agent verifies the HMAC, rebuilds the LangGraph
from the recipe stashed by `create_llm_agent`, and the new
`PollingA2aTool` (or MCP tool) appears in the next LLM call's tool
list. No restart.

### Step 9 — Verify and roll out

In order, before flipping production traffic:

1. **Boot the agent locally with `SWARMD_*` vars set.** Look for the
   banner with the agent name, then `Found N subscribed agents` and
   `Loaded N MCP tool(s) from M server(s)` in the logs. Check
   credentials and base URL if either is missing.
2. **`curl /.well-known/agent-card.json`** — the response should
   match the agent's name, description, and at least the synthetic
   skill.
3. **Send a message that exercises a sub-agent** via
   `POST /` (JSON-RPC `message/send`). Confirm the LLM picks the right
   `PollingA2aTool`, that the call shows up in the audit log under a
   single correlation ID, and that the response makes its way back
   even if the downstream went through a `working` state first.
4. **Send a message that exercises an MCP tool.** Confirm the same.
5. **Rotate a subscription in the dashboard.** Watch `/admin/webhook`
   fire and `Loaded N+/-1 MCP tool(s)` reappear in logs. No restart.
6. **Roll out.** The migration is purely additive on the agent
   process — the same binary runs unconfigured (standalone) or
   configured (SwarmD-attached) depending on env, so you can canary
   it.

## Summary

**What changed.**

* `main.py` shrinks to domain logic. No more bespoke HTTP tools, no
  more hand-rolled FastAPI / Starlette server.
* Outbound calls go through the relay. You get audit, policy, HITL,
  and rate limits for free.
* Sub-agents and MCP tools are discovered from subscriptions. Each
  one is a standard LangChain `BaseTool`.
* Long-running sub-agents stop blocking your LLM —
  `PollingA2aTool` drives `tasks/get` until terminal.
* `/admin/webhook` keeps the agent's tool catalogue in sync without
  restarts.

**What didn't.**

* Your `@tool`-decorated functions.
* Your `agent.ainvoke({"messages": [...]})` call shape from anything
  that already calls the agent locally.
* Your existing LangChain integrations on the graph (chat history,
  memory, structured output) — they live on the `CompiledStateGraph`
  returned by `create_llm_agent`.
* Local-only behaviour: with `SWARMD_*` unset the agent runs as a
  standalone LangChain agent against direct OpenAI.

For the Google ADK equivalent of this guide, see
[Migrating from Google ADK](./migration-google-adk).
