Migrating from LangGraph
This page is for teams with a working LangGraph project who want to know what a Dawn conversion actually costs.
Dawn does not replace LangGraph. A graph's nodes, edges, imported tools, and state definitions can stay in place, while the deployment and invocation boundary still needs validation.
What changes is the code around the graph: project layout and deploy config. Dawn's co-located tool convention applies to agent and workflow routes, while sibling state.ts is agent-only; a raw graph keeps its own definitions.
The migration is mostly moving code, not rewriting it.
tl;dr
- Your
StateGraphnodes and edges can stay. Export the compiled object as a namedgraphroute. - Your graph can keep its imported tools; Dawn agent and workflow tools can use co-located TypeScript files instead.
- Your raw graph keeps its existing channel and state definitions. Dynamic segment values still come from the caller's JSON state.
- Your
langgraph.jsonis replaced bydawn build. The output is still alanggraph.json. - Model providers and LangChain packages can stay; validate checkpointer configuration at each target boundary.
- LangSmith consumes the build output generated by
dawn build.
The shape of the move
Before — a typical LangGraph TypeScript project:
my-agents/
├── langgraph.json
├── package.json
├── tsconfig.json
└── src/
├── graphs/
│ ├── support.ts
│ └── triage.ts
├── tools/
│ ├── lookupOrder.ts
│ └── escalate.ts
└── state.tsAfter — the same project under Dawn:
my-agents/
├── .dawn/dawn.generated.d.ts
├── dawn.config.ts
├── package.json
├── tsconfig.json
└── src/
├── app/
│ ├── support/
│ │ └── index.ts
│ └── triage/
│ └── index.ts
├── graphs/
│ ├── support.ts
│ └── triage.ts
├── tools/
│ ├── lookupOrder.ts
│ └── escalate.ts
└── state.tsFlat directories named by kind become folder routes named by endpoint. Tools you move into Dawn's tool convention can live next to a route; an imported graph keeps its existing tool imports. The route registry — what graph answers which path — is read from the file tree, not maintained by hand.
Construct by construct
StateGraph → route
The graph's nodes and edges do not need to change. Its hand-maintained assistant_id registration is replaced by a small route module that re-exports the compiled graph.
Before:
import { StateGraph, START, END } from "@langchain/langgraph"
import type { SupportState } from "../state.js"
import { lookupOrder } from "../tools/lookupOrder.js"
import { escalate } from "../tools/escalate.js"
export const support = new StateGraph<SupportState>({
channels: {
messages: { reducer: (a, b) => [...a, ...b], default: () => [] },
orderId: null,
},
})
.addNode("lookup", async (state) => {
const result = await lookupOrder.invoke({ orderId: state.orderId })
return { messages: [result] }
})
.addNode("escalate", async (state) => {
await escalate.invoke({ reason: "no order" })
return state
})
.addEdge(START, "lookup")
.addEdge("lookup", END)
.compile()After — the route re-exports the real compiled graph without changing its nodes, edges, or imported tool calls:
export { support as graph } from "../../graphs/support.js"The route re-exports the graph object you authored. The folder path src/app/support/ becomes the endpoint /support; only its exported name changes to Dawn's graph route convention. The generated assistant_id is /support#graph.
If the graph is the only thing you want to migrate, this completes the route entry. Tools and state can stay imported from their old locations; validate the runtime boundary described next before cutover.
The Dawn local HTTP runtime does not translate its Agent Protocol thread id into a precompiled raw graph's configurable.thread_id. If that graph's checkpointer requires the configurable id, add an explicit target-boundary wrapper or configuration adaptation and validate that target boundary before cutover. Do not assume a checkpointer that worked behind another server receives the same invocation config from Dawn.
Raw graph state stays with the graph
A raw graph route does not use a sibling state.ts. Keep the graph's existing TypeScript state type, channels, reducers, and defaults with the compiled graph; callers send the JSON state that graph already expects.
Dynamic folders still describe a parameterized route id rather than injecting values. For /support/[tenant], the caller includes { "tenant": "acme", ... } in the JSON state, with the field name aligned to the segment. Sibling state.ts, its defaults, and reducers/ are Dawn agent-route features; adopt them only when deliberately converting the graph to an agent route. See State for that path.
LangChain tools → Dawn agent and workflow tools
When converting behavior to a Dawn agent or workflow route, tools become default-exported async functions in the route's tools/ directory. Type inference at build time replaces every hand-written schema. A raw graph may instead keep its existing LangChain tools and imports unchanged.
Before:
import { tool } from "@langchain/core/tools"
import { z } from "zod"
export const lookupOrder = tool(
async ({ orderId }: { orderId: string }) => {
const res = await fetch(`https://api.example.com/orders/${orderId}`)
return await res.json()
},
{
name: "lookupOrder",
description: "Look up an order by id.",
schema: z.object({ orderId: z.string() }),
},
)After:
export default async (
input: { readonly orderId: string },
ctx: { signal: AbortSignal },
) => {
const res = await fetch(`https://api.example.com/orders/${input.orderId}`, {
signal: ctx.signal,
})
return (await res.json()) as { readonly status: string }
}The file basename is the tool name. The input type is read from the parameter annotation. The output type is read from the return type. dawn typegen writes both into .dawn/dawn.generated.d.ts; dawn build uses the generated tool schemas when materializing deployment entries.
Inside an agent route, the LLM picks when to invoke. A workflow(state, ctx) receives RuntimeContext, so it can call ctx.tools.lookupOrder({ orderId }) with full IntelliSense. A raw graph route does not receive ctx.tools; an existing graph keeps calling the tools it imports.
Conditional edges and routing → middleware + dispatch
Two different mechanisms in LangGraph become two different mechanisms in Dawn. Don't conflate them.
Graph-level conditional edges stay where they are. addConditionalEdges is a runtime concern of the graph. Dawn does not touch it.
.addConditionalEdges("triage", (state) => {
if (state.priority === "p0") return "escalate"
return "respond"
})Request-level branching — auth, tenant gating, routing requests between assistants — moves to middleware.ts. The middleware decides whether the request runs at all and what context flows into tools.
Before — branching inside the graph entry point:
app.post("/runs/wait", async (req, res) => {
if (!req.headers["x-api-key"]) return res.status(401).end()
const which = req.body.tenant === "internal" ? internalGraph : publicGraph
const result = await which.invoke(req.body.input)
res.json(result)
})After:
import { allow, defineMiddleware, reject } from "@dawn-ai/sdk"
export default defineMiddleware(async (req) => {
if (!req.headers["x-api-key"]) return reject(401, { error: "Missing x-api-key" })
return allow({ tenant: req.params.tenant ?? "public" })
})Routing between assistants is a route concern: /support/internal and /support/public are two routes, each with its own graph. The route id is the dispatch.
langgraph.json → dawn build
The hand-maintained config becomes a build output.
Before:
{
"dependencies": ["."],
"graphs": {
"support": "./src/graphs/support.ts:support",
"triage": "./src/graphs/triage.ts:triage"
},
"env": ".env"
}After — there is no source langgraph.json. There is a dawn.config.ts:
export default {
appDir: "src/app",
}dawn build walks src/app/, runs typegen, and writes .dawn/build/langgraph.json plus per-route entry files. Every route's assistant_id is <routeId>#<kind> — /support#graph, /support/[tenant]#agent. That .dawn/build/ directory is what LangSmith deploys.
.dawn/dawn.generated.d.ts is the type side of the same step: the route registry, the tool registry, the typed RouteTools<P> map. The starter template ignores .dawn/, so regenerate it during development and CI unless your project chooses to commit generated artifacts.
What can stay, with boundary validation
- LangSmith. Tracing, evaluations, datasets — Dawn does not wrap or proxy. Set
LANGSMITH_API_KEYand traces flow;dawn devauto-setsLANGCHAIN_TRACING_V2=truewhen that key is present. - Checkpointer and persistence. The checkpointer attached with
.compile({ checkpointer })remains attached to the raw graph. Its required invocation config does not appear automatically: Dawn's local Agent Protocol thread id is not translated intoconfigurable.thread_id, so adapt and test any graph that relies on that value. - Model providers. Raw
graphandchainroutes keep whatever LangChain-compatible providers you instantiate yourself. The built-inagent()route materializes to a LangChain chat model; Dawn infers providers for known model families and lazy-loads the matching LangChain integration package. Setproviderexplicitly to one of the supported built-in provider ids for aliases, ambiguous model names, local models, or provider-router model ids. - LangChain ecosystem packages.
@langchain/core,@langchain/openai, retrievers, document loaders — every one works inside a route. - LangSmith deploy. Same target.
dawn buildemits the generatedlanggraph.jsonand entry files LangSmith consumes.
Migration order
The conversion is incremental. Don't try to land it in one branch.
-
Scaffold a Dawn project alongside the existing one. Run
pnpm create dawn-ai-app my-agents-dawnand let it generate the scaffold. Don't merge the two repos yet. The existing project keeps shipping; the Dawn project is where the new shape lives. -
Move one graph at a time, route by route. Pick the lowest-risk graph first. Create
src/app/<route>/index.tsand named-export the existingStateGraphasgraph— tools, state, and prompts keep their old import paths. Once it deploys and runs at parity in staging, repeat with the next graph. -
Cut over deployment last. Both projects can deploy to LangSmith side by side under different
assistant_ids. When every graph has a Dawn equivalent at parity, switch the productionassistant_ids to the Dawn-built ones and retire the old project.
What to read next
- I want to scaffold a Dawn project now. → Getting Started
- I want the boundary in one page. → Mental Model
- I want construct-level depth. → Routes, Agents, Tools, State, Middleware