> ## 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](/sdks/python/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](/sdks/python/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](/sdks/python/langchain).

## Migration steps

### Step 1 — Register the agent

In the [dashboard](https://app.dev.swarmd.ai) (or via
`POST /registry/v1/agents`, body `{"name": "...", "description": "..."}`),
register `weather_agent`. Capture the three values 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.

<Note>
  Registration is two calls, and this is only the first. What you hold now is an
  *unbootstrapped shell* — an identity with no agent card behind it. It can mint
  tokens, which is all you need for the rest of this migration, but it cannot be
  subscribed to anything and cannot be sent a webhook until you attach a card
  with `PUT /registry/v1/agents/{agentId}`.

  Do that once the migrated agent is running on a URL Swarmd can reach — it is
  [Step 7 of Setup](/sdks/python/setup#step-7-attach-your-agent-card), and it has
  to land before Step 8 below will work.
</Note>

### 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.dev.swarmd.ai
SWARMD_TOKEN_URL=https://auth.dev.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](/sdks/python/langchain#dissected-create_llm_agent).

**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](/sdks/python/langchain#dissected-serve).

**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. Provided the runtime has the webhook secret (see the warning in Step 3
above), 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. Without the secret, the delivery is refused with `503` and
you have to `POST /admin/refresh` by hand after each grant change.

### 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 MCP tool(s) from M server(s)` reappear in the logs
   with the new count. No restart. If the webhook secret never reached the
   runtime you'll see a `503` instead — `POST /admin/refresh` to confirm the
   rebuild itself works.
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](/sdks/python/migration-google-adk).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.