Your first agent
From an empty directory to a deployed agent server. No LangGraph experience assumed — the parts of it you need are explained here, where you need them.
If you already have a LangGraph.js graph or a langgraph.json, you want getting-started.md instead: there is nothing to scaffold, and adopting skein is a one-line change.
Prerequisites: Node 24 is recommended, with Node 22.12+ within 22.x also supported, and a package manager. Nothing in this guide needs an API key until you choose a model, and nothing needs Docker until deployment. For a workspace, start with the monorepo guidance.
1. Create the project
npm create skein-js@latest my-agentpnpm create skein-js@latest my-agent and yarn create skein-js@latest my-agent do the same thing. Keep @latest to request the latest published scaffolder.
Interactive runs ask about the model provider, ESLint and Prettier, local storage, dependency installation, and git initialization. Pick "None" for the provider, keep memory storage and ESLint/Prettier enabled, and accept dependency installation. The directory is already supplied above; it is prompted when omitted. --yes accepts defaults without prompting.
You can add a model in a minute. The generated echo graph runs without credentials.
Then:
cd my-agent
npm run devYou now have an agent server on http://localhost:2024.
2. Look at it before you read any code
Open http://localhost:2024/console.
That's the skein console, served by your own process — no account, no hosted service, no tunnel. In Playground, choose the echo Assistant, send "hello", and watch the run execute. You will get echo: hello back, because the graph you just created echoes its input.
This is worth doing before anything else: everything below is about changing what happens between your message and that reply, and it helps to have seen the loop close.
Run the generated graph tests in a second terminal:
npm testBoth echo cases should pass without a model key or a running server. As you change the graph, update its behavior assertions; Testing your project adds an SDK smoke test and checks for multi-turn state, persistence, and approval flows.
3. What you actually got
The project includes tests, default ESLint/Prettier tooling, and a .nvmrc selecting a supported Node version. The three files to understand first:
src/echo-graph.ts — your agent. We'll come back to it.
langgraph.json — how skein finds your graphs:
{
"node_version": "24",
"graphs": { "echo": "./src/echo-graph.ts:graph" },
"env": ".env"
}"./src/echo-graph.ts:graph" means the export named graph in that file. The key — echo — is what clients ask for by name. This is the same file format the LangGraph CLI reads, which is why skein is a drop-in for it.
package.json — the commands that are the whole lifecycle:
npm run dev | what you're running: hot reload, in-memory state, zero setup |
npm run dev:services | Postgres + Redis in Docker, for the durable dev and start |
npm run dev:postgres | the same hot reload, against those services instead |
npm run build | compile your graphs to plain JavaScript in .skein/ |
npm start | serve that build — this is what production runs |
(If you picked Postgres at the storage prompt, dev is already the durable one and the in-memory spelling is dev:memory instead — both are always there.)
The rest: .env (ready to use — the model key is commented out, the service URIs are not) and .env.example (a committed reference copy of it), compose.dev.yaml (the services for start), tsconfig.json, a test, and a README.
4. Understanding the graph
Here is the whole agent:
import { AIMessage, type BaseMessage } from "@langchain/core/messages";
import { MessagesAnnotation, StateGraph } from "@langchain/langgraph";
function echo(state: typeof MessagesAnnotation.State): { messages: BaseMessage[] } {
const last = state.messages.at(-1);
const text = typeof last?.content === "string" ? last.content : "";
return { messages: [new AIMessage(`echo: ${text}`)] };
}
export const graph = new StateGraph(MessagesAnnotation)
.addNode("echo", echo)
.addEdge("__start__", "echo")
.addEdge("echo", "__end__")
.compile();Four ideas, and they are the only LangGraph concepts you need to get started:
State is the data flowing through your agent. MessagesAnnotation is a ready-made state that holds a list of chat messages and knows to append new ones rather than replace the list. Most conversational agents want exactly this.
A node is a function. It receives the current state and returns only the part that changed — here, one new message. It is a plain function: you can call it, test it, and step through it in a debugger.
Edges say what runs next. __start__ and __end__ are the built-in entry and exit points, so this graph is "start → echo → done." Real agents branch here — that is where the "graph" part earns its name.
.compile() turns the definition into something runnable. What it returns is what skein serves.
Try it: change echo: to you said: in src/echo-graph.ts and save. The server hot-reloads and keeps your existing threads — send another message in the console and you will see the new reply in the same conversation.
Those four ideas are enough to finish this guide. When you want the rest — custom state, branching, subgraphs, interrupt() — LangGraph essentials is the short version, with a link out to the LangChain docs on each one.
5. Give it a real model
Now swap the echo for an LLM. Install a provider:
npm install @langchain/anthropic langchain zodCreate src/agent-graph.ts:
import { ChatAnthropic } from "@langchain/anthropic";
import { createAgent } from "langchain";
const model = new ChatAnthropic({ model: "claude-sonnet-5", temperature: 0 });
export const graph = createAgent({ model, tools: [] });createAgent is LangChain's prebuilt agent loop: call the model, and if it asks for a tool, run the tool and call the model again. You did not have to build that loop. It lives in the langchain package — LangGraph v1 deprecated its own createReactAgent and moved it there.
Register it in langgraph.json:
{
"graphs": {
"echo": "./src/echo-graph.ts:graph",
"agent": "./src/agent-graph.ts:graph"
},
"env": ".env"
}Add your Anthropic API key to the existing .env file:
ANTHROPIC_API_KEY=your-api-keyThe packages installed above provide the model, createAgent, and the tool schema used in the next step. Keep the existing env setting in langgraph.json when adding the new graph.
Restart, pick agent in the console, and you are talking to a real model. Tokens stream as they are generated.
Prefer to skip this assembly?
npm create skein-js@latest my-agent -- --provider anthropicscaffolds all of it, plus a working tool, in one step.
6. Give it a tool
Talking is half of it. Tools are what let an agent actually do things:
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const getWeather = tool(
async ({ city }: { city: string }) => {
const geo = await fetch(
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`,
).then((response) => response.json());
const place = geo.results?.[0];
if (!place) return `I couldn't find ${city}.`;
const forecast = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${place.latitude}` +
`&longitude=${place.longitude}¤t=temperature_2m`,
).then((response) => response.json());
return `It's ${forecast.current.temperature_2m}°C in ${place.name}.`;
},
{
name: "get_weather",
description: "Get the current weather for a city.",
schema: z.object({ city: z.string().describe("City name, e.g. 'Nairobi'") }),
},
);Pass it in — createAgent({ model, tools: [getWeather] }) — and ask "what's the weather in Nairobi?". The console shows the model deciding to call the tool, the result coming back, and the final answer. (Open-Meteo needs no API key of its own.)
The description and schema are how the model knows when to use it. Write them for a reader who has no other context, because that is exactly the model's situation.
7. Talk to it from your own code
The server speaks the standard Agent Protocol, so the official SDK works — there is no skein client:
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "http://localhost:2024" });
const thread = await client.threads.create();
for await (const event of client.runs.stream(thread.thread_id, "agent", {
input: { messages: [{ role: "user", content: "what's the weather in Nairobi?" }] },
})) {
console.log(event.event, event.data);
}For a UI, the useStream React hook talks to the same server with only a URL change — see react-sdk.md. Vue, Svelte and Angular work too; so do Agent Chat UI and LangGraph Studio.
8. Remember things between conversations
Threads persist a conversation. A store persists across conversations — what a user told you last week. Any node can reach it:
import { getStore } from "@langchain/langgraph";
async function remember(state: typeof MessagesAnnotation.State) {
const store = getStore();
await store.put(["users", "alice"], "prefers", { units: "celsius" });
const saved = await store.get(["users", "alice"], "prefers");
// …use saved.value in your prompt
}The default dev uses memory. A server can use Postgres with vector search for semantic recall; a local application can use SQLite with text/filter search. Your node code does not change. See storage.md and memory.md.
9. Ship it
dev is not the production path. Production is build + start:
npm run dev:services # Postgres + Redis via Docker
npm run build # graphs → plain JavaScript in .skein/build
npm startNothing to edit first: the POSTGRES_URI and REDIS_URI in .env already match those services, and npm start loads them itself.
build compiles your TypeScript graphs ahead of time; start serves that output with no TypeScript toolchain in the loop. It is exactly what the production container runs.
The default start selects Postgres and Redis for durable state, queued work, and multiple instances. It is one deployment option. For a desktop app, CLI, daemon, or single Node owner, choose SQLite: scaffold with --storage sqlite to generate SQLite dev/start scripts without Compose. The same graphs, checkpoints, and protocol are used; the application decides whether to retry an abandoned attempt.
For a container, npx skein up brings up the whole stack, and npx skein build produces an image. Then pick a host: Cloud Run, Fly, Railway, Render, AWS, Kubernetes, or a plain VPS. Full guide: deploy.md.
Where to next
Read building blocks. It is the map of everything this guide skipped — checkpoints, threads, multitask, interrupts, memory, what production actually needs — a few lines each, so you can tell which page you want before you need it.
If you would rather understand the graph model first, LangGraph essentials covers state, reducers, branching, subgraphs and interrupt(), and links out to the LangChain docs on each.
Then, as you need them:
- Recipes — auth, human-in-the-loop, background runs, CORS
- The console — test your graph and inspect its output, state, and failures
- Agent Protocol — every endpoint your server exposes
- Scaffolding reference — every flag, and the Nx generators