Migrate existing AI agent projects (LangChain, LangGraph, OpenAI Agents SDK, Claude Agent SDK, CrewAI) to EdgeOne Makers platform conventions. Use when the user wants to adapt a standard agent project to run on EdgeOne Makers, convert Express/Next.js API routes to Makers handlers, or add platform capabilities (context.tools, context.sandbox, context.store). Do NOT trigger for new agent projects (use makers-agents instead).
79
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
Migrate existing AI agent projects to the EdgeOne Makers platform format. Covers structural conversion, API adaptation, and platform capability injection.
What type of project are you migrating?
├── Python project
│ ├── Using CrewAI → See §2 CrewAI
│ ├── Using LangChain/LangGraph/DeepAgents → See §3 LangGraph (Python)
│ ├── Using OpenAI Agents SDK → See §4 OpenAI Agents (Python)
│ └── Using Claude Agent SDK → See §5 Claude SDK (Python)
└── Node/TS project
├── Using Express/Next.js API routes → See §6 Express → Makers
├── Using LangGraph/DeepAgents → See §3 LangGraph (Node)
├── Using OpenAI Agents SDK → See §4 OpenAI Agents (Node)
└── Using Claude Agent SDK → See §5 Claude SDK (Node)Before starting framework-specific changes, check these global items:
edgeone.json with correct agents.framework and buildCommand/outputDirectoryagents/ directoryprocess.env / os.environ with context.env / ctx.envreq.headers.get('x') with context.request.headers['x'] (Node) or plain dict access (Python)await req.json() with context.request.body (already parsed)AI_GATEWAY_* env varsres.json() / return {"data": ...})makers-conversation-id header to frontend fetch callscontext.tools instead of custom tool implementationscontext.store instead of in-memory or custom DBWSA_API_KEY env var and use context.tools.get("web_search")edgeone makers dev for local developmentThis is the most common migration pattern. Applies to Express/Next.js API routes, plain HTTP handlers, etc.
// ❌ Before: Next.js API route (app/api/chat/route.ts)
export async function POST(req: Request) {
const body = await req.json();
const headers = req.headers;
const apiKey = process.env.OPENAI_API_KEY;
// ... LLM call ...
return Response.json({ data: result });
}
// ✅ After: Makers agent handler (agents/chat/index.ts)
export async function onRequest(context: any) {
const body = context.request.body; // already parsed
const conversationId = context.conversation_id; // auto-injected from header
const env = context.env; // context.env, never process.env
// ... LLM call via AI_GATEWAY_* ...
return new Response(JSON.stringify({ data: result }), {
headers: { 'Content-Type': 'application/json' },
});
}# ❌ Before: Flask route
@app.route('/chat', methods=['POST'])
def chat():
body = request.get_json()
api_key = os.environ.get('OPENAI_API_KEY')
# ... LLM call ...
return jsonify({'data': result})
# ✅ After: Makers agent handler (agents/chat/index.py)
async def handler(ctx):
body = ctx.request.body
conversation_id = ctx.conversation_id
api_key = ctx.env.get("AI_GATEWAY_API_KEY")
# ... LLM call via AI_GATEWAY_* ...
return {"data": result}| Before | After |
|---|---|
os.environ.get("OPENAI_API_KEY") | ctx.env.get("AI_GATEWAY_API_KEY") |
LLM(provider="openai", ...) — LiteLLM dispatch | LLM(provider="openai", base_url=ctx.env["AI_GATEWAY_BASE_URL"], ...) — bypass LiteLLM |
memory=True on Crew | memory=False + use ctx.store |
verbose=True | verbose=False (events go through crewai_event_bus) |
crew.kickoff() (blocking) | await asyncio.to_thread(crew.kickoff) |
| Custom search tools | Use ctx.tools.to_crewai_tools(BaseTool) |
| Flask/FastAPI handler | async def handler(ctx): → ctx.utils.stream_sse(gen()) |
{
"buildCommand": "",
"outputDirectory": "",
"agents": {
"framework": "crewai"
}
}crewai>=1.14.5
openai>=1.50.0async def handler(ctx):ctx.env, never os.environLLM(provider="openai", api_key=ctx.env["AI_GATEWAY_API_KEY"], base_url=ctx.env["AI_GATEWAY_BASE_URL"])Crew(memory=False, verbose=False)crew.kickoff() in asyncio.to_thread()ctx.tools.to_crewai_tools(BaseTool)ctx.utils.stream_sse(gen())See makers-agents/skills/python-frameworks/crewai.md for the complete pattern. Detailed before/after: references/crewai-to-makers.md
| Before | After |
|---|---|
Direct model creation (new ChatOpenAI(...)) | Use AI_GATEWAY_* for apiKey/baseURL |
MemorySaver (in-memory checkpointer) | context.store.langgraphCheckpointer (persistent) |
| Custom tool functions | context.tools.toLangChainTools(tool) |
agent.stream() | SSE via createSSEResponse(gen, signal) (Node) or ctx.utils.stream_sse(gen()) (Python) |
thread_id manual management | thread_id = context.conversation_id |
{
"agents": {
"framework": "langgraph"
}
}{
"buildCommand": "",
"outputDirectory": "",
"agents": {
"framework": "langgraph"
}
}agents/<name>/index.ts (or .py)AI_GATEWAY_API_KEY + AI_GATEWAY_BASE_URLcontext.store.langgraphCheckpointer instead of MemorySavercontext.store.langgraphStorecontext.tools.toLangChainTools(tool) instead of custom tool functionsthread_id: { configurable: { thread_id: context.conversation_id } }Node: makers-agents/skills/node-frameworks/langgraph.md Python: makers-agents/skills/python-frameworks/langgraph.md DeepAgents: makers-agents/skills/node-frameworks/deepagents.md Detailed before/after: references/langgraph-to-makers.md, references/deepagents-to-makers.md
| Before | After |
|---|---|
new OpenAI({ apiKey, baseURL }) | Read AI_GATEWAY_* from context.env / ctx.env |
Runner.run(agent, input, { tools }) | Tools from context.tools.all() (already OpenAI function format) |
| Session management | context.store.openaiSession(convId) (Node) |
| Express route response | SSE via createSSEResponse(gen, signal) (Node) or ctx.utils.stream_sse(gen()) (Python) |
| Model name hardcoded | `ctx.env.AI_GATEWAY_MODEL |
{
"agents": {
"framework": "openai-agents-sdk"
}
}{
"buildCommand": "",
"outputDirectory": "",
"agents": {
"framework": "openai-agents-sdk"
}
}agents/<name>/index.ts (or .py)context.env (not process.env)context.tools.all() (returns OpenAI function tools)context.store.openaiSession(conversationId) for session (Node)output_text_delta → ai_response, tool_called → tool_callNode: makers-agents/skills/node-frameworks/openai-agents.md Python: makers-agents/skills/python-frameworks/openai-agents.md Detailed before/after: references/openai-agents-to-makers.md
| Before | After |
|---|---|
ANTHROPIC_API_KEY env var | Mapped from AI_GATEWAY_* via collectGatewayEnv() |
process.env | context.env injected into query().options.env |
| Custom MCP tools | context.tools.toClaudeMcpServer() |
| Session | context.store.claudeSessionStore() (Node) |
| Stdout EPIPE crash | Swallow EPIPE on process.stdout (Node) |
| No writable config dir | Set CLAUDE_CONFIG_DIR=/tmp/claude-agent-sdk, CLAUDE_CODE_TMPDIR=/tmp |
{
"agents": {
"framework": "claude-agent-sdk"
}
}{
"buildCommand": "",
"outputDirectory": "",
"agents": {
"framework": "claude-agent-sdk"
}
}agents/<name>/index.ts (or .py)AI_GATEWAY_* → ANTHROPIC_* via collectGatewayEnv(context.env)query({ options: { env: collectGatewayEnv(...) } })context.tools.toClaudeMcpServer('edgeone', { alwaysLoad: true })EPIPE on process.stdoutCLAUDE_CONFIG_DIR=/tmp/claude-agent-sdk, CLAUDE_CODE_TMPDIR=/tmpNode: makers-agents/skills/node-frameworks/claude-sdk.md Python: makers-agents/skills/python-frameworks/claude-sdk.md Detailed before/after: references/claude-agent-sdk-to-makers.md
General migration for any Express-based or Next.js API route agent.
| Step | Before | After |
|---|---|---|
| 1. File location | app/api/chat/route.ts or server/routes/chat.ts | agents/chat/index.ts |
| 2. Entry signature | export async function POST(req) or app.post('/chat', handler) | export async function onRequest(context) |
| 3. Body parsing | await req.json() | context.request.body (already parsed) |
| 4. Headers | req.headers.get('x-foo') | context.request.headers['x-foo'] |
| 5. Abort signal | req.signal | context.request.signal (AbortSignal) |
| 6. Model access | process.env.OPENAI_API_KEY → direct call | context.env.AI_GATEWAY_* → AI Gateway |
| 7. Response | res.json() or return Response.json() | SSE stream via createSSEResponse(gen, signal) |
// ❌ Before: Next.js (app/api/chat/route.ts)
import { NextRequest } from 'next/server';
import OpenAI from 'openai';
export async function POST(req: NextRequest) {
const { message } = await req.json();
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const response = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: message }],
stream: true,
});
// ... stream back as Response
}
// ✅ After: EdgeOne Makers (agents/chat/index.ts)
import { createLogger, sseEvent, createSSEResponse } from '../_shared';
export async function onRequest(context: any) {
const { message } = context.request.body ?? {};
if (!message) return new Response('Missing message', { status: 400 });
const signal = context.request.signal as AbortSignal;
return createSSEResponse(async function* (sig) {
const response = await fetch(context.env.AI_GATEWAY_BASE_URL + '/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${context.env.AI_GATEWAY_API_KEY}`,
},
body: JSON.stringify({
model: context.env.AI_GATEWAY_MODEL || '@makers/deepseek-v4-flash',
messages: [{ role: 'user', content: message }],
stream: true,
}),
signal: sig,
});
// ... proxy SSE chunks ...
yield 'data: [DONE]\n\n';
}, signal);
}// ❌ Before: plain fetch without conversation-id
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});
// ✅ After: with makers-conversation-id header
const conversationId = getOrCreateConversationId(); // crypto.randomUUID() + localStorage
const response = await fetch('/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'makers-conversation-id': conversationId, // ⭐ required for all AI endpoints
},
body: JSON.stringify({ message }),
});// ✅ Always pass conversation_id in body for /stop
await fetch('/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ conversation_id: conversationId }),
});const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
try {
const event = JSON.parse(data);
if (event.type === 'ai_response') { /* display text */ }
if (event.type === 'tool_call') { /* show tool call */ }
if (event.type === 'ping') { /* ignore heartbeat */ }
} catch { /* skip non-JSON */ }
}
}
}After migration, verify these items before deploying:
edgeone makers dev starts without errors/chat endpoint returns SSE stream (not JSON)context.env is used everywhere (grep for process.env / os.environ — none should remain)edgeone.json has correct agents.frameworkcontext.tools) work in at least one frameworkcontext.store)/stop endpoint cancels active runsmakers-conversation-id headeronRequest/handler)77de93b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.