Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/app/components/CreamSurface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { ReactNode } from "react"
export function CreamSurface({ children }: { readonly children: ReactNode }) {
const isLanding = usePathname() === "/"
return (
<div className={isLanding ? "" : "bg-bg-primary text-text-primary min-h-screen"}>
<div className={isLanding ? "h-full" : "bg-bg-primary text-text-primary h-full"}>
{children}
</div>
)
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/components/ReadingLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ interface ReadingLayoutProps {
export function ReadingLayout({ left, right, children }: ReadingLayoutProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-[240px_minmax(0,1fr)] lg:grid-cols-[240px_minmax(0,1fr)_240px] xl:grid-cols-[280px_minmax(0,1fr)_240px]">
<aside className="hidden md:block sticky top-16 self-start h-[calc(100vh-4rem)] overflow-y-auto px-6 py-8 border-r border-border-subtle">
<aside className="hidden md:block sticky self-start overflow-y-auto px-6 py-8 border-r border-border-subtle top-[var(--header-h)] h-[calc(100vh-var(--header-h))]">
{left}
</aside>
<section className="min-w-0 px-6 md:px-12 py-12 max-w-[760px] mx-auto w-full">
{children}
</section>
<aside className="hidden lg:block sticky top-16 self-start h-[calc(100vh-4rem)] overflow-y-auto px-6 py-8 border-l border-border-subtle">
<aside className="hidden lg:block sticky self-start overflow-y-auto px-6 py-8 border-l border-border-subtle top-[var(--header-h)] h-[calc(100vh-var(--header-h))]">
{right}
</aside>
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/components/blog/PostMeta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ export function PostMeta({ post }: { readonly post: Post }) {
<Image
src={author.avatar}
alt={author.name}
width={36}
height={36}
width={28}
height={28}
className="rounded-full"
/>
<a
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/components/docs/DocsTOC.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function DocsTOC() {
if (headings.length === 0) return null

return (
<nav aria-label="On this page" className="sticky top-8 w-52 shrink-0 hidden xl:block text-sm">
<nav aria-label="On this page" className="sticky top-8 w-52 shrink-0 hidden lg:block text-sm">
<p className="text-xs text-text-muted uppercase tracking-widest mb-3">On this page</p>
<ul className="space-y-2 border-l border-border-subtle">
{headings.map((h) => (
Expand Down
7 changes: 7 additions & 0 deletions apps/web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
--dawn-white: #ffffff;
--dawn-neutral-gray: #6b6b6b;
--dawn-font-sans: var(--font-inter), "Satoshi", "Helvetica Neue", Arial, sans-serif;
/* Chrome height — Header has px-6/8 + py-4 + ~h-10 brand row + 1px border.
Used by ReadingLayout sticky offsets and anchor scroll padding. */
--header-h: 4.5rem;
}

html {
scroll-padding-top: var(--header-h);
}

/* Landing dark scope — landing-section components author against the dark token
Expand Down
99 changes: 73 additions & 26 deletions apps/web/content/blog/2026-05-12-why-we-built-dawn.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,62 @@ type: post
author: brian
---

Dawn started as a folder of helper scripts that kept showing up in every LangGraph project we touched. By the third project, it was clear the helpers were a framework wearing a trench coat.
Agent codebases are becoming one of the harder shapes in TypeScript right now.

This post is about the friction those scripts were absorbing, and why we decided to make that work explicit.
Not because the runtimes are bad.
And not because the model providers have failed us.

The runtime is fine. LangGraph holds up. What does not hold up is the *code around* the graph.

That is the gap Dawn was built to close.

## tl;dr

- dawn is a meta-framework for LangGraph, not a replacement for it
- the runtime, the channels, the checkpointer, LangSmith — all unchanged
- `dawn build` emits a `langgraph.json` that deploys to LangSmith without translation
- the thing dawn replaces is the boilerplate, the registry files, and the implicit project layout
- agent code should feel like Next.js code: filesystem routes, inferred types, a real dev loop

## The moment it stopped scaling

LangGraph is excellent at what it does. The runtime, the channels, the checkpointer — they all hold up. What didn't hold up was the *code around* the graph.
Dawn started as a folder of helper scripts that kept showing up in every LangGraph project I touched.

By the third project, it was clear the helpers were a framework wearing a trench coat.

The trigger was a single project. Four graphs, eleven tools, and a `langgraph.json` that nobody wanted to touch. Graphs in one folder. Tools in another. State shapes in a third. A `registry.ts` gluing them together. Every new feature touched all four locations.

Our second production agent project had four graphs, eleven tools, and a `langgraph.json` that no one wanted to touch. The graphs lived in one folder, the tools in another, the state shapes in a third, and a `registry.ts` glued them together. Every new feature touched all four locations.
I started writing internal docs explaining where things were supposed to go.

We started writing internal docs explaining where things were supposed to go. That was the moment. If the framework is making you write a memo about file layout, the framework is missing a piece.
That was the moment.

If the framework is making you write a memo about file layout, the framework is missing a piece.

## The friction list

The same pain showed up in every project. None of it is LangGraph's fault — these are just things LangGraph leaves to you.
The same pain shows up in every LangGraph project past the second graph. None of it is LangGraph's fault. These are things LangGraph leaves to you.

- **graph boilerplate.** every route involves the same `StateGraph` plumbing — `addNode`, `addConditionalEdges`, `bindTools`, an exported `compile()`. the interesting code is twenty lines; the wiring is sixty.
- **no project structure.** langgraph has no opinion on where graphs, tools, or state live. so every team invents a layout, then re-invents it six months later.
- **hand-written tool schemas.** every tool means a function, a zod schema, and a `tool()` wrapper. the schema duplicates the typescript types you already wrote.
- **no local dev loop.** `langgraph dev` works but is heavyweight. iterating against a route meant pushing to staging.
- **lost types at the boundary.** tool inputs, state shape, dynamic routing parameters — none typed across the graph boundary by default.

Each item is solvable on its own.

The trouble is that every team solves them slightly differently, and the solutions do not compose.

That is the real cost. Not the boilerplate. The lack of a shared shape.

- **Graph boilerplate.** Every agent route involves the same `StateGraph` plumbing — `addNode`, `addConditionalEdges`, `bindTools`, an exported `compile()`. The interesting code is twenty lines; the wiring is sixty.
- **No project structure.** LangGraph has no opinion on where graphs, tools, or state live. So every team invents their own layout, then re-invents it six months later when the first one stops working.
- **Hand-written tool schemas.** Every tool means a function, a Zod schema, and a `tool()` wrapper. The schema duplicates the TypeScript types you already wrote, and drifts the moment you forget.
- **No local dev loop.** `langgraph dev` works but is heavyweight. Iterating against a route meant pushing to staging, watching the deploy, and waiting on traces. Tight loops aren't tight.
- **Lost types at the boundary.** Tool inputs, state shape, dynamic routing parameters — none of these are typed across the graph boundary by default. You learn at runtime what a misspelled key costs.
## What "meta-framework" actually means here

Each item is solvable on its own. The trouble is that every team solves them slightly differently, and the solutions don't compose.
Dawn is not a competitor to LangGraph.

## What "meta-framework" means here
It is the layer above it.

Dawn is not a competitor to LangGraph. The runtime, the graph compiler, the checkpointer, LangSmith — all unchanged. The output of `dawn build` is a `langgraph.json` that LangSmith deploys without translation.
The runtime, the graph compiler, the checkpointer, LangSmith — all unchanged. The output of `dawn build` is a `langgraph.json` that LangSmith deploys without translation.

What Dawn replaces is the *code around* the graph. Concretely:
What Dawn replaces is the *code around* the graph.

Without Dawn, a hello-world tool-calling agent looks like this:

Expand Down Expand Up @@ -76,7 +103,7 @@ export const graph = new StateGraph(MessagesAnnotation)

Plus a hand-maintained `langgraph.json` pointing at the export.

With Dawn, it's two files:
With Dawn, it is two files:

```ts
// src/app/(public)/hello/[tenant]/index.ts
Expand All @@ -93,24 +120,44 @@ export default agent({
export default async ({ name }: { name: string }) => `Hello, ${name}!`
```

`dawn build` produces the `langgraph.json`. The `greet` tool is bound to the agent because it lives under `tools/`. The `[tenant]` segment becomes a typed field on state. Same deploy target; roughly a quarter of the code.
`dawn build` produces the `langgraph.json`. The `greet` tool is bound to the agent because it lives under `tools/`. The `[tenant]` segment becomes a typed field on state.

The point isn't lines saved. The point is that every concept — route, tool, state, middleware — has *one place* it lives, and that place is on disk where your editor can find it.
Same deploy target. Roughly a quarter of the code.

The point is not lines saved.

The point is that every concept — route, tool, state, middleware — has one place it lives. And that place is on disk where your editor can find it.

That is the difference between "we wrote some helpers" and "we built a framework."

## The bet: agent code should feel like Next.js code

The Next.js App Router did one thing extraordinarily well: it gave every concern a coordinate. A route is a folder. A layout is `layout.tsx`. A server action is a function in a route file. You can point at a thing, search for it, refactor it, write a test next to it.
The Next.js App Router did one thing extraordinarily well.

It gave every concern a coordinate.

A route is a folder. A layout is `layout.tsx`. A server action is a function in a route file. You can point at a thing, search for it, refactor it, write a test next to it.

Agent codebases lack that. The runtime is reasonable, but the codebase shape is whatever you negotiated with your team last quarter.

My bet is that agent code should feel like Next.js code. A route is a folder under `src/app/`. The folder path is the agent endpoint. Tools, state, middleware, and tests live next to the route they belong to. Types are generated from the file tree, not maintained by hand.

That is the whole framework.

Filesystem routes. Inferred types. A real dev server. A build step that emits the artifact LangSmith already understands.

## Where Dawn is now

Agent codebases lack that. The runtime is reasonable, but the *codebase shape* is whatever you negotiated with your team last quarter.
Dawn 0.3 ships the route conventions, the typed tool inference, the dev server, and the build step.

Our bet is that agent code should feel like Next.js code. A route is a folder under `src/app/`. The folder path is the agent endpoint. Tools, state, middleware, and tests live next to the route they belong to. Types are generated from the file tree, not maintained by hand.
The mental model is documented at [`/docs/mental-model`](/docs/mental-model). The route conventions live at [`/docs/routes`](/docs/routes). If you have a working LangGraph project, the migration walkthrough at [`/docs/migrating-from-langgraph`](/docs/migrating-from-langgraph) covers the construct-by-construct move.

That's the whole framework. Filesystem routes, inferred types, a real dev server, and a build step that emits the artifact LangSmith already understands.
What is next is Deep Agents — composable sub-agents — and a deeper streaming story. Both ship in 0.4.

## Where we are now
If you have felt the friction described above, the [getting started](/docs/getting-started) guide takes about a minute end to end.

Dawn 0.3 ships the route conventions, the typed tool inference, the dev server, and the build step. The mental model is documented at [`/docs/mental-model`](/docs/mental-model) and the route conventions at [`/docs/routes`](/docs/routes). If you have a working LangGraph project, the migration walkthrough at [`/docs/migrating-from-langgraph`](/docs/migrating-from-langgraph) covers the construct-by-construct move.
That is the right way to evaluate whether Dawn matches the mental model you already have.

What's next is Deep Agents — composable sub-agents — and a deeper streaming story. Both are coming in 0.4.
The runtime is becoming policy. The framework around it is becoming product behavior.

If you've felt the friction described above, the [getting started](/docs/getting-started) guide takes about a minute end to end. That's the right way to evaluate whether Dawn matches the mental model you already have.
That is what makes this interesting.
Loading
Loading