# Migrating from LangChain to Managed Skills: A Step-by-Step Guide

> Source: https://vivgrid.com/blog/migrating-from-langchain-to-managed-skills

You don't have to rewrite your LangChain agent to get managed tooling. Here's how to migrate your tools to Vivgrid Managed Skills, step by step.

![A bridge carrying glowing blocks from a tangled cluster to a clean cloud platform — a migration motif](/images/blog/migrating-from-langchain-to-managed-skills-hero.png)

Let's get one thing out of the way: LangChain is good software. It has the largest ecosystem in the agent space, it's genuinely great for prototyping, and if you built your agent on it, you made a reasonable choice. This post is not a hit piece.

It's a migration guide — and a deliberately boring one, in the best sense. The move from LangChain to Vivgrid's [Managed Skills](/docs/managed-skills) platform doesn't require a rewrite. You don't touch your orchestration, you don't abandon LangGraph, and you don't re-architect anything. You migrate the _tools_ first, one function at a time, and leave everything else exactly where it is.

Why bother? Because the tools are where the operational pain lives. They're the part of your agent that holds API keys, makes network calls to flaky third-party services, needs logging and retry logic, and scales differently from the rest of your app. Moving tools to managed, cloud-hosted skills gives you versioning, observability, and secret management for the riskiest part of your agent — while your orchestration code stays home in LangChain.

Honest framing, up front: migrate tools first, orchestration later — or never. Plenty of teams will keep their LangGraph graphs forever and be perfectly happy. This guide respects that.

## What stays and what moves

Before the steps, a map of the migration:

| Component                              | Stays in your code                               | Moves to Vivgrid                 |
| -------------------------------------- | ------------------------------------------------ | -------------------------------- |
| Orchestration (chains, graphs, memory) | ✅ stays                                         | —                                |
| Tool definitions (`@tool` functions)   | —                                                | ✅ becomes Managed Skills        |
| System prompt                          | —                                                | ✅ console-managed               |
| Model selection                        | —                                                | ✅ console-managed, unified API  |
| API keys and secrets                   | —                                                | ✅ `viv deploy --env`            |
| Client calls                           | updated to point at `https://api.vivgrid.com/v1` | —                                |

Notice what this implies: your LangChain agent keeps deciding _what to do_. Vivgrid just takes over _how the tools run_ — and the prompt and model the agent runs under. The orchestration logic you spent weeks tuning doesn't get rewritten; it gets a new backend for its tools.

## Step 1: Inventory your tools

Start with a census. List every `@tool`-decorated function (or `StructuredTool` subclass) in your agent, and note what each one does, what secrets it needs, and whether it has side effects.

```typescript
// A typical LangChain tool inventory might look like this:
import { tool } from "@langchain/core/tools";

export const getOrderStatus = tool(
  async ({ orderId }: { orderId: string }) => {
    // calls internal order API, needs ORDER_API_KEY
    return await fetchOrderStatus(orderId);
  },
  { name: "get_order_status", description: "Look up an order's shipping status" }
);

export const issueRefund = tool(
  async ({ orderId, amount }: { orderId: string; amount: number }) => {
    // calls payment provider, needs STRIPE_KEY — has side effects!
    return await createRefund(orderId, amount);
  },
  { name: "issue_refund", description: "Issue a refund for an order" }
);

export const searchKnowledgeBase = tool(
  async ({ query }: { query: string }) => {
    // calls vector DB, needs PINECONE_KEY
    return await vectorSearch(query);
  },
  { name: "search_knowledge_base", description: "Search support articles" }
);
```

For each tool, write down:

- **Secrets**: which environment variables it reads (`ORDER_API_KEY`, `STRIPE_KEY`, …)
- **Side effects**: read-only tools are safe to test freely; write tools (refunds, emails, ticket creation) need shadow/sandbox handling
- **Timeouts and retries**: tools calling third-party APIs probably already have retry logic — you'll keep that inside the skill

This inventory is your migration checklist. A typical support agent has 5–15 tools; migrate them in order of risk, read-only first.

## Step 2: Convert each tool to a typed Managed Skill

A Managed Skill is the same function you already wrote, packaged as a typed, versioned, cloud-hosted unit. The conversion is mostly mechanical: take the tool's logic, give it explicit typed inputs, and export it in the skill format.

**Before** — your LangChain tool:

```typescript
import { tool } from "@langchain/core/tools";

export const getOrderStatus = tool(
  async ({ orderId }: { orderId: string }) => {
    const res = await fetch(`https://orders.acme.com/v1/orders/${orderId}`, {
      headers: { Authorization: `Bearer ${process.env.ORDER_API_KEY}` },
    });
    if (!res.ok) throw new Error(`order API: ${res.status}`);
    const order = await res.json();
    return { status: order.status, eta: order.eta };
  },
  {
    name: "get_order_status",
    description: "Look up an order's shipping status by order ID",
  }
);
```

**After** — the same logic as a Vivgrid Managed Skill:

```typescript title="skills/get-order-status.ts"
// A typed Managed Skill definition
// (deployed with `viv deploy`; see vivgrid.yml in Step 3)
export default {
  name: "get_order_status",
  description: "Look up an order's shipping status by order ID",
  input: {
    type: "object",
    properties: {
      orderId: { type: "string", description: "The order ID to look up" },
    },
    required: ["orderId"],
  },
  handler: async ({ orderId }: { orderId: string }, ctx: SkillContext) => {
    const res = await fetch(`https://orders.acme.com/v1/orders/${orderId}`, {
      headers: { Authorization: `Bearer ${ctx.env.ORDER_API_KEY}` },
    });
    if (!res.ok) throw new Error(`order API: ${res.status}`);
    const order = await res.json();
    return { status: order.status, eta: order.eta };
  },
};
```

Three things changed, and only three:

1. **The tool logic moved into a `handler`.** Same code, same retries, same error handling.
2. **Inputs became an explicit schema.** You probably had this implicitly via Zod or a type annotation — now it's declared, which gives you validation and a contract the model can see.
3. **Secrets come from `ctx.env`, not `process.env`.** This is the whole point: your `.env` file stops being the security boundary. Secrets get injected at deploy time (Step 3).

Do this once per tool. Read-only tools first, write tools after you've verified the pattern works. The orchestration code still references the old `@tool` functions for now — you'll repoint it in Step 5.

## Step 3: Move secrets to `viv deploy --env`

This is the step your security team will thank you for. Right now your secrets live in a `.env` file on whatever machine runs the agent — a laptop, a VM, a container with a long and storied history. Managed Skills take secrets as deploy-time environment variables instead:

```bash
viv deploy . \
  --env ORDER_API_KEY=<redacted> \
  --env STRIPE_KEY=<redacted> \
  --env PINECONE_KEY=<redacted>
```

After this, delete those keys from your local `.env`. The skills read them from their managed environment at runtime (`ctx.env.ORDER_API_KEY`), and they're never in your repo, your laptop, or your chat logs.

The following environment variables are required for the example agent above — yours will differ, but the pattern is the same:

- `ORDER_API_KEY`: API key for the internal order lookup service
- `STRIPE_KEY`: payment provider key (used only by the `issue_refund` skill)
- `PINECONE_KEY`: vector database key for the knowledge-base search skill

Prepare your `vivgrid.yml` before deploying. It declares your project and its skills:

```yaml title="vivgrid.yml"
project: acme-support-agent

skills:
  - name: get_order_status
    entry: ./skills/get-order-status.ts
  - name: issue_refund
    entry: ./skills/issue-refund.ts
  - name: search_knowledge_base
    entry: ./skills/search-knowledge-base.ts

model:
  provider: anthropic        # or openai, google, deepseek — one API, one key
  name: claude-sonnet-4-5   # configured here, in the console, not in client code
```

A note on secret rotation: because secrets are deploy-time configuration rather than code, rotating a key is `viv deploy --env STRIPE_KEY=<new>` — no code change, no rebuild, no redeploy pipeline. Your future self, rotating keys at 2am during an incident, says thanks.

## Step 4: Move the system prompt and model choice to the console

Your LangChain agent currently carries its system prompt and model selection in code — something like:

```typescript
// Before: prompt and model baked into the client
const model = new ChatOpenAI({ model: "gpt-4o", temperature: 0.2 });
const agent = createReactAgent({
  llm: model,
  tools: [getOrderStatus, issueRefund, searchKnowledgeBase],
  prompt: SUPPORT_SYSTEM_PROMPT, // a long string in your repo
});
```

Move both to the Vivgrid console. The system prompt becomes runtime configuration: edit it in the console and every conversation that starts afterward uses the new text, with no code change and no release. The model choice becomes console configuration on the unified model API — pick GPT, Claude, Gemini, or DeepSeek, and your API calls don't change.

Why this matters beyond convenience: it shortens incidents. When a prompt edit goes wrong in production, the fix is another edit in the console — not a code change, not a redeploy. If you've ever paged someone to "just revert the prompt change," you know exactly how valuable that is.

Copy your existing system prompt into the console as-is. Don't rewrite it during migration; the goal is a behavior-preserving move. Keep the original in git until you've cut over, so you can always paste it back. You'll iterate on it later, with evals gating every change.

## Step 5: Repoint the client at `https://api.vivgrid.com/v1`

Now the satisfying part. Your client code gets simpler: point it at the Vivgrid unified model API and drop the `model` field — the model is [configured in the console](/docs/quick-start) now (Step 4), so the API call doesn't need to name it.

```typescript
import OpenAI from "openai";

// Before: provider-specific client, model hardcoded in code
// const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// After: one endpoint, one Vivgrid key, model chosen in the console
const client = new OpenAI({
  apiKey: process.env.VIVGRID_API_KEY, // viv-xxxxxxxxxx from the console
  baseURL: "https://api.vivgrid.com/v1",
});

const completion = await client.chat.completions.create({
  // no `model` field — the console decides which model serves this project
  messages: [
    { role: "system", content: SUPPORT_SYSTEM_PROMPT },
    { role: "user", content: userMessage },
  ],
  tools: managedSkillTools, // your skills, exposed as function tools
});
```

If you're keeping LangChain orchestration (and most teams do at this stage), the change is narrower than it looks: your LangGraph graph, your memory, your routing logic all stay. What changes is where tools execute and where the model comes from. The agent's _brain_ is the same; its _hands_ are now managed.

## Step 6: Deploy with `viv deploy`

With skills written, secrets injected, and the prompt and model in the console, deploy:

```bash
viv deploy . \
  --env ORDER_API_KEY=<redacted> \
  --env STRIPE_KEY=<redacted> \
  --env PINECONE_KEY=<redacted>
```

What this gives you per skill: a versioned deployment, request logs, latency and cost-per-request [observability](/docs/observability), and **Agent Insights** into how each tool is actually being called — which arguments the model passes, where calls fail, which skills are expensive. The things you currently learn from `console.log` and hope, now as a dashboard.

Deploy to a development project first. Run your manual smoke tests there — the same conversations you'd test locally. Only when the dev project behaves, promote the skill versions to production. This project separation is your staging environment, and it costs nothing extra to maintain.

## Step 7: Verify with an Alchemist regression eval before cutting over

Don't cut over on vibes. Before you route real traffic to the migrated skills, run [**Alchemist**](/docs/agent-evaluation) — Vivgrid's evaluation suite — against your deployment as a regression gate.

The idea is straightforward: assemble a set of representative conversations (your existing test cases, plus a sample of real production transcripts), run them against the new skill-backed agent, and score the outcomes — task completion, tool-call accuracy, and cost. Alchemist runs both recursive evals (multi-turn, agentic) and regression evals (same suite, every deploy), so you get a before/after comparison, not just a one-time snapshot.

A practical cutover checklist:

1. ✅ All tools converted and deployed to the dev project
2. ✅ Secrets injected via `--env`, removed from local `.env`
3. ✅ System prompt and model configured in the console (unchanged from the original)
4. ✅ Alchemist regression eval passes — scores at or above your LangChain baseline
5. ✅ Write tools (`issue_refund`, email senders, ticket creators) tested in shadow/sandbox mode first
6. ✅ Canary: route 5% of traffic, watch Agent Insights, then ramp

If an eval fails, the failure tells you exactly which skill diverged — because skills are versioned independently, you fix and redeploy that one skill, not the whole agent. That's the payoff of the migration: the blast radius of a tool change shrinks to the tool.

## Your orchestration code can stay

This deserves its own section because it's the question everyone asks: _do I have to rewrite my LangGraph workflows?_

No. LangChain's orchestration — graphs, subgraphs, memory, human-in-the-loop interrupts — is genuinely good at what it does, and none of it depends on where your tools execute. A LangGraph node that calls a tool doesn't care whether that tool is a local Python function or a managed skill behind an API. Keep your graphs. Keep your state management. Keep the parts you tuned for weeks.

The migration this guide describes moves the _execution surface_ of your tools — where they run, how they're secured, how they're observed — and leaves the _decision surface_ — what to call, when, in what order — exactly where it is. Many teams will stop here permanently: LangChain orchestration on top, Managed Skills underneath. That's not a halfway migration. That's the architecture.

If you later decide to move orchestration too, you'll do it with the same playbook: incrementally, one workflow at a time, with evals gating every step. But there's no deadline.

## Troubleshooting

**"My skill works locally but fails after deploy."**

Nine times out of ten, this is a missing environment variable — the skill read `process.env.X` locally and you haven't injected it with `--env`. Check the skill's request logs in the console; the error will name the missing key. Also confirm you switched every `process.env` reference to `ctx.env`.

**"Tool calls are slower than before."**

Your tools now execute over the network instead of in-process. For most API-backed tools the added latency is small relative to the third-party call itself. If a skill is chatty — called many times per turn with tiny payloads — consider batching its inputs so one skill call does the work of five. Agent Insights shows per-skill latency, so measure before optimizing.

**"The model calls my skill with wrong arguments."**

This is a schema problem, not a model problem. Tighten the skill's input schema: precise descriptions, required fields, enums where the values are bounded. Then check the console logs for the actual arguments the model passed — the fix is usually one sentence in the skill description. This is also exactly what the Alchemist regression suite catches before production does.

**"I'm getting auth errors on the unified API."**

Make sure you're using your Vivgrid key (`viv-...` from the console), not your old provider key, and that `baseURL` is exactly `https://api.vivgrid.com/v1`. And remember: no `model` field in the request — if you pass one, it conflicts with the console-configured model.

**"Can I migrate just one tool to try it out?"**

Yes — that's the recommended way to start. Pick a read-only tool, convert it, deploy it, and have your LangChain agent call it alongside the local tools. One skill in production teaches you more about the platform than ten in a slide deck.

## Migrate the tools, keep the agent

The whole migration, summarized: inventory your `@tool` functions, convert each to a typed Managed Skill, move secrets to `viv deploy --env`, move the prompt and model to the console, repoint the client at `https://api.vivgrid.com/v1` without a `model` field, deploy, and verify with an Alchemist regression eval before cutting over. Your LangGraph orchestration stays. Your timelines stay sane. Your security team gets secrets out of `.env` files.

LangChain got you from zero to a working agent — credit where it's due. Managed Skills take you from a working agent to one you can operate: versioned tools, server-side prompts, and evals gating every change. Migrate the tools first. The orchestration can follow — or never.

---

_Vivgrid is the Managed Skills platform: cloud-hosted LLM function calling that lets enterprise AI agents run their tools in the cloud — with observability, evaluation, and globally distributed inference built in. Start with the [Quick Start](/docs/quick-start), see [pricing](/pricing), or talk to us at [hi@vivgrid.com](mailto:hi@vivgrid.com)._
