Skip to content

Using skein-js in your app ​

A dense, task-oriented cheat-sheet for consuming skein-js — pick a framework, stand up an Agent Protocol server around your LangGraph.js graph, and call it. Written to be skim-friendly for humans and for AI coding agents building on skein. (Working on skein itself? See AGENTS.md.)

The one thing to know: the { config } | { deps } seam ​

Every framework adapter takes the same options bag (SkeinRuntimeOptions). You choose one of two inputs, plus optional common fields:

ts
// EITHER: let skein build an in-memory runtime from a langgraph.json (dev / zero-setup)
{ config: "./langgraph.json", importModule? }
// OR: bring your own assembled ProtocolDeps (production drivers, custom auth, in-code graphs)
{ deps }
// plus common: { logger?, cors?, warm? }

config → in-memory drivers, hot-reload, great for dev. deps → whatever you assembled (Postgres + Redis for production, or an in-code graph map). Same server either way — only the wiring differs.

Install ​

Starting from nothing? npm create skein-js@latest my-agent writes the project for you and you can skip this section.

Otherwise pick your framework adapter; @langchain/langgraph is always a peer dependency (bring your graph).

bash
pnpm add @skein-js/express  @langchain/langgraph        # Express
pnpm add @skein-js/fastify  @langchain/langgraph        # Fastify
pnpm add @skein-js/nestjs   @langchain/langgraph        # NestJS
pnpm add @skein-js/nextjs   @langchain/langgraph        # Next.js

For production drivers add @skein-js/runtime (assembles Postgres/Redis). Prefer the CLI on-ramp? pnpm add -D skein-js and run skein dev — a drop-in for langgraph dev.

Three on-ramps ​

A — You have nothing yet. Scaffold a working project — a langgraph.json, a keyless graph, a test, and the dev/build/start lifecycle:

bash
npm create skein-js@latest my-agent

See scaffolding.md, or your-first-agent.md for the walkthrough.

B — You have a langgraph.json (or use the LangGraph CLI today). Change one script and keep the config unchanged:

diff
- "dev": "langgraph dev",
+ "dev": "skein dev",

Or point an adapter at the config: { config: "./langgraph.json" }. See langgraph-cli-compat.md.

C — You have a compiled graph in code (no config, no CLI). Wrap it into deps and pass { deps }:

ts
import { createExpressServer } from "@skein-js/express";
import { embedInMemoryGraphs } from "@skein-js/server-kit";
import { graph } from "./my-graph.js";

const server = await createExpressServer({ deps: embedInMemoryGraphs({ agent: graph }) });
await server.listen(2024);

embedInMemoryGraphs(graphs, { overrides? }) builds a ProtocolDeps (store, queue, bus, checkpointer). See embedding.md.

Mount it on your framework ​

Each adapter ships a standalone server (create*Server) and an embed-alongside-your-app path. All accept the { config } | { deps } seam above.

ts
// Express — standalone, or skeinRouter({...}) to mount on an existing app
import { createExpressServer } from "@skein-js/express";
const server = await createExpressServer({ config: "./langgraph.json" });
await server.listen(2024);

// Fastify — standalone, or app.register(skeinPlugin, { prefix: "/agent", config })
import { createFastifyServer } from "@skein-js/fastify";
await (await createFastifyServer({ config: "./langgraph.json" })).listen(2024);

// NestJS — imports: [SkeinModule.forRoot({ config: "./langgraph.json" })]
import { createNestServer } from "@skein-js/nestjs";
await (await createNestServer({ config: "./langgraph.json" })).listen(2024);

// Next.js — App Router catch-all: app/api/[...path]/route.ts
import { createSkeinRouteHandlers } from "@skein-js/nextjs";
export const runtime = "nodejs";
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = createSkeinRouteHandlers({ deps });

Each adapter has a standalone entry (a dedicated graph server) and an embed-alongside-your-app entry, each with a runnable example:

FrameworkPackageStandalone (dedicated server)Embed in an existing appExamples
Express@skein-js/expresscreateExpressServerskeinRouter (mount the Router)express-basic, embed-graph
Fastify@skein-js/fastifycreateFastifyServerskeinPlugin (register under a prefix)fastify-basic, fastify-app
NestJS@skein-js/nestjscreateNestServerSkeinModule.forRoot (import it)nestjs-basic, nestjs-app
Next.js@skein-js/nextjs— (the route handlers are the server)createSkeinRouteHandlers (App Router) · createSkeinPagesHandler (Pages)nextjs-app, nextjs-basic

embed-graph is the framework-agnostic in-code pattern (embedInMemoryGraphs + { deps }, no langgraph.json) shown on the Express adapter; the same deps works with any adapter above. react-usestream is a browser frontend for any of them. See Expand your setup to grow from here.

Where to point your client when embedding ​

The protocol is served wherever you mount it, so set the client's apiUrl to the mount root, not the server root. How you set the mount differs per adapter:

ts
// Express — mount the router under a path
const { router } = await skeinRouter({ deps });
app.use("/agent", router); // → apiUrl: http://localhost:2024/agent

// Fastify — the plugin is encapsulated, so `prefix` isolates skein's routes + CORS
await app.register(skeinPlugin, { prefix: "/agent", deps }); // → .../agent

// NestJS — no skein-side option: it follows your app's global prefix
app.setGlobalPrefix("api"); // → apiUrl: http://localhost:2024/api

// Next.js — the catch-all's location, via `basePath` (defaults to "/api")
// app/api/[...path]/route.ts                                → apiUrl: http://localhost:3000/api

Mount at the root (no prefix) and apiUrl is just the server root. NestJS is the odd one out: it reads the mount from the framework rather than from an argument you pass, so there is no skein-side option to keep in sync with setGlobalPrefix.

Go to production (Postgres + Redis) ​

Swap the in-memory deps for durable ones — everything else stays the same. Two ways:

ts
// In code: durable deps around graphs you hold (reads POSTGRES_URI / REDIS_URI)
import { embedPostgresGraphs } from "@skein-js/runtime";
import { createExpressServer } from "@skein-js/express";

const { deps, dispose } = await embedPostgresGraphs({ agent: graph });
const server = await createExpressServer({ deps });
await server.listen(2024);
process.on("SIGTERM", () => dispose().then(() => process.exit(0)));
ts
// From a langgraph.json: pick drivers explicitly
import { buildRuntime } from "@skein-js/runtime";
const rt = await buildRuntime({
  configPath: "./langgraph.json",
  store: "postgres",
  queue: "redis",
});
const server = await createExpressServer({
  deps: rt.deps,
  channels: rt.channels,
  cors: rt.cors,
});

Redis is optional but required to run more than one instance (the in-memory queue is process-local). Or skip the code entirely: skein dev --store postgres --queue redis, and skein build / skein up for a container. See embedding.md, storage.md, runs-and-redis.md.

Call the server ​

Any Agent Protocol client works — no custom SDK. The two you'll reach for:

ts
// Node / server-to-server — @langchain/langgraph-sdk
import { Client } from "@langchain/langgraph-sdk";
const client = new Client({ apiUrl: "http://localhost:2024" });
const thread = await client.threads.create();
const input = { messages: [{ role: "user", content: "hello" }] };
const reply = await client.runs.wait(thread.thread_id, "agent", { input });
for await (const ev of client.runs.stream(thread.thread_id, "agent", { input })) console.log(ev);
tsx
// Browser — @langchain/langgraph-sdk/react useStream (SSE)
import { useStream } from "@langchain/langgraph-sdk/react";
const thread = useStream({ apiUrl: "http://localhost:2024", assistantId: "agent" });
thread.submit({ messages: [{ type: "human", content: "hello" }] });

assistantId defaults to the graph_id. See react-sdk.md, streaming.md.

Endpoint surface ​

skein implements the standard Agent Protocol REST + SSE contract, so the SDK maps onto it directly. The resources: assistants (a served graph + its schemas), threads (persistent conversations), runs (/runs/wait, /runs/stream, background /threads/{id}/runs with join + cancel), and a long-term store (/store/items, semantic /store/items/search). The full endpoint inventory and the auth route→permission map live in agent-protocol.md.

Package → import map ​

You want to…ImportFrom
Serve on Express / Fastify / Nest / Nextcreate*Server / skein* / SkeinModule@skein-js/<framework>
Embed a graph in code (in-memory)embedInMemoryGraphs@skein-js/server-kit
Embed a graph in code (durable Postgres)embedPostgresGraphs@skein-js/runtime
Assemble prod deps from a langgraph.jsonbuildRuntime@skein-js/runtime
Implement a storage driver / handle edge errorsSkeinStore, SkeinHttpError@skein-js/core
Put skein on a framework we don't shipskeinRoutes, createProtocolRuntime@skein-js/agent-protocol

Expand your setup ​

Grow from the minimal server without rewrites — each step changes one thing:

  • Add another graph. Add an entry to the graph map (embedInMemoryGraphs({ echo, agent })) or to langgraph.json's graphs. Each becomes an assistant, addressed by its graph_id.
  • Standalone → embedded. Move from a dedicated create*Server to the embed entry for your framework (skeinRouter / skeinPlugin / SkeinModule.forRoot / createSkein*Handlers) — see the adapter table above — to serve the protocol next to your existing routes, under a prefix if you want.
  • Go durable / scale out. Swap the in-memory deps for embedPostgresGraphs(...) or buildRuntime({ store: "postgres", queue: "redis" }). Add Redis to run more than one instance. See Go to production.
  • Drain more background runs at once. Each instance executes 10 queued runs concurrently by default — tune it with skein dev --concurrency 4, SKEIN_RUN_CONCURRENCY=4, or worker: { maxConcurrency: 4 } on any adapter. See run concurrency.
  • Add auth, memory, HITL, webhooks. These are drop-in — see the recipes (custom auth, getStore() long-term memory, interrupt/resume, run-completion webhooks).
  • A framework we don't ship. The adapters are thin shims over one transport-neutral handler table (createProtocolRuntime + skeinRoutes); put skein on any Node HTTP framework by writing ~40 lines of request/response mapping. See building-an-adapter.md.

Gotchas ​

  • Auth is off by default. No auth block / no auth dep → the server is fully open, exactly like langgraph dev. Turn it on with a @langchain/langgraph-sdk/auth Auth instance — see the provider recipes and production recipe.
  • CORS is off by default. Browser clients on another origin need http.cors in langgraph.json (or the cors option). Same-origin (e.g. Next.js) needs nothing.
  • A long-lived Node process is required for the background run worker and in-memory drivers — fine on a normal server / next start; for serverless, use Postgres + Redis.
  • useStream needs an absolute URL — pass `${window.location.origin}/api`, not a bare /api.
  • Bundling skein yourself (rspack/webpack/esbuild, or an unusual Next.js config)? skein is ESM-only but require()-resolvable, and @langchain/langgraph-api + @typescript/vfs must stay external. See bundling.md.
  • 404s on every protocol path? You're almost certainly pointing at the wrong root — the protocol lives at your mount path, not the server root (see Where to point your client). On NestJS that means app.setGlobalPrefix("api") moves it to /api/threads. Two red herrings worth ruling out: an Unsupported route path: "/api/*" warning in a NestJS boot log is Nest auto-converting the adapter's catch-all and is harmless, and /info isn't part of the surface — a 404 there is correct.

Go deeper ​