Essay · 7 min read
Why we built Dawn
Dawn is a TypeScript-first framework for building LangGraph.js agents with file-system routes, route-local tools, generated types, and a local dev loop.
I built Dawn because agent codebases were starting to feel harder to maintain than they needed to be.
Not because LangGraph.js is the wrong runtime.
In fact, I like the runtime. I like the graph model. I like that LangSmith gives teams a real deployment target.
The problem I kept running into was the code around the graph: the project layout, the tool wiring, the generated artifacts, the local feedback loop, and the small conventions every team has to invent before they can build the thing they actually care about.
This is the problem Dawn is trying to solve.
The short version
Dawn is a TypeScript-first framework for building LangGraph.js agents.
The goal is not to replace LangGraph.js. The goal is to make the application around LangGraph.js easier to build, test, and deploy.
In practice, Dawn gives you:
- file-system routes under
src/app/ - route-local tools with input and output types inferred from TypeScript
- optional route state through
state.ts - a local
dawn devruntime with/runs/waitand/runs/stream dawn typegenfor.dawn/dawn.generated.d.tsdawn buildfor.dawn/build/langgraph.jsonand per-route LangGraph entry files- built-in agent behavior for memory, planning, skills, and subagents
That list is intentionally practical. It is the stuff you otherwise end up writing by hand.
The problem I kept seeing
The first version of Dawn was not a framework.
It was a folder of helpers that kept showing up in LangGraph.js projects.
One project had four graphs, eleven tools, a hand-written langgraph.json, and a registry file that existed mostly to explain the rest of the files to each other. Graphs lived in one directory. Tools lived in another. State definitions lived somewhere else. Tests drifted from the route they were meant to protect.
Every new feature touched too many places.
At some point I started writing internal documentation that explained where new code was supposed to go.
That was the signal.
If I need a memo to explain the file layout, the framework is missing a piece.
What I wanted instead
I wanted the same thing I like about good web application frameworks: a boring answer to "where does this code go?"
A route should be a folder.
A tool should live with the route that uses it.
State should have one file.
The dev server should notice changes.
The build step should produce the artifact the runtime expects.
Types should follow the code instead of asking the developer to copy a shape from one file into another.
None of that is especially magical. That is the point. The best framework conventions usually feel obvious after you use them for a week.
What a route looks like
A Dawn route is just a folder under src/app/.
For example:
src/
app/
(public)/
hello/
[tenant]/
index.ts
state.ts
tools/
greet.tsThe route id is /hello/[tenant]. The route group (public) is ignored, the same way route groups work in modern web frameworks.
The route entry can be an agent() descriptor:
// src/app/(public)/hello/[tenant]/index.ts
import { agent } from "@dawn-ai/sdk"
export default agent({
model: "gpt-4o-mini",
systemPrompt: "You are a helpful assistant for the {tenant} organization.",
})And a tool is a plain TypeScript function:
// src/app/(public)/hello/[tenant]/tools/greet.ts
export const description = "Greet a user by name."
export default async ({ name }: { readonly name: string }) => {
return `Hello, ${name}!`
}There is no tool() wrapper here.
There is no hand-written Zod schema for the tool input.
Dawn reads the function signature, generates the tool schema for the model, and writes the typed route registry to .dawn/dawn.generated.d.ts. The same discovered tool is wired into generated agent deployment entries when you run dawn build.
That is the core bet: your TypeScript source should be the contract.
What happens at build time
dawn build does three things that I used to do by hand:
- It discovers the routes in
src/app/. - It runs type generation for route state and route-local tools.
- It writes
.dawn/build/langgraph.jsonplus per-route entry files.
For an agent route, the generated entry imports your default agent() descriptor, materializes it as a LangGraph graph, wires in the route-local tools, and maps the assistant id to the generated graph export.
So a route like /hello/[tenant] becomes an assistant id like:
/hello/[tenant]#agentThis matters because the Dawn project structure stays a TypeScript authoring experience, while the output is still a LangGraph-compatible deployment package.
What we have built to date
The foundation of Dawn is routing, tools, types, a dev server, and build output.
That proves the basic shape, but real agent applications need more than a single tool-calling loop.
Here is what we have built to date around that route structure.
Memory
If workspace/AGENTS.md exists, Dawn injects it into the agent prompt under a # Memory heading on every model turn.
This is intentionally simple. The file is the memory. The model sees the current contents. If your app updates the file, the next turn sees the updated memory.
There is a size limit and there are no hidden databases involved. That is a tradeoff I like for this layer.
Skills
A route can include skills under:
src/app/<route>/skills/<name>/SKILL.mdDawn lists the available skills in the prompt and exposes a readSkill({ name }) tool so the agent can load the full instructions when it needs them.
This keeps long-form instructions out of the prompt until they are useful.
Planning
A route with a plan.md file opts into the planning capability.
Dawn gives the agent a writeTodos tool, a todos state channel, and a plan_update stream event. The agent can maintain a real plan during multi-step work, and the runtime can stream those updates to a UI.
The important part is not the todo list itself. The important part is that a capability can add tools, prompt fragments, state, and stream behavior as one unit.
Subagents
Subagents are becoming a first-class composition boundary.
A parent route can expose child agents through the subagents field on agent({...}), or by placing child routes under a subagents/ directory. Dawn adds a task({ subagent, input }) tool and a prompt section that tells the parent what specialists are available.
That makes a subagent feel less like an imported helper function and more like a route with its own prompt, tools, and state.
Reasoning effort
agent({...}) also accepts:
reasoning: { effort: "high" }For OpenAI-backed agent routes, Dawn maps that to the model's reasoning effort parameter when the model supports it. Non-reasoning models ignore it.
That is the kind of option I want in the descriptor: close to the route, explicit, and easy to review in code.
What Dawn is not
It is important to be precise here.
Dawn is not a new model provider abstraction.
Dawn is not trying to hide LangGraph.js.
Dawn is not necessary for every agent project.
If you have one graph, two tools, and no need for a framework, raw LangGraph.js may be the better choice.
But if your project is starting to need route conventions, generated types, a dev server, tests beside the routes, generated deployment artifacts, and reusable agent behavior, then the framework starts to earn its place.
Try it
The fastest way to get a feel for the framework is still the scaffold:
pnpm create dawn-ai-app my-agents
cd my-agents
pnpm devThen run the example route:
echo '{"tenant":"acme"}' | pnpm exec dawn run '/hello/[tenant]'If you want the mental model first, start with Mental Model. If you want the file conventions, start with Routes. If you have an existing LangGraph.js project, the migration guide walks through the move construct by construct.
Conclusion
Dawn exists because I wanted agent code to have a shape that editors, tests, dev servers, and deployment tools could all understand.
The runtime is still LangGraph.js.
The difference is the application structure around it.
Routes are folders. Tools are local. Types are generated. Agent behavior composes around the route. The build output is explicit.
That is the ordinary work of a framework.
And for agent applications, I think that ordinary work matters.