@langchain/core
@langchain/core is the foundation package of LangChain.js: the base abstractions everything else in that ecosystem implements. It defines chat models, messages, prompt templates, tools, output parsers, retrievers, and the Runnable interface that lets you pipe these pieces together with streaming, batching, and fallbacks built in. You rarely install it as your only dependency; provider packages like @langchain/openai and @langchain/anthropic extend its base classes, and the langchain and langgraph packages build on top of it.
If you are in the LangChain ecosystem, core is not optional, and its abstractions are genuinely well designed for provider swapping and streaming. If you are not already committed to that ecosystem, start with a plain SDK or the Vercel AI SDK and adopt LangChain only when you feel the need for its orchestration.
Use it if
- You already use langchain, langgraph, or any @langchain/* provider package: core is their shared contract and you will import messages and prompts from it constantly
- You are writing a custom integration (a chat model wrapper, retriever, or tool) that should plug into the LangChain ecosystem: extending core's base classes is the whole point of the package
- You want provider-agnostic app code where swapping ChatOpenAI for ChatAnthropic is a one-line change because both satisfy the same BaseChatModel interface
- You need streaming, batching, retries, and fallbacks as composable wrappers instead of hand-rolled loops around a provider SDK
- You call one LLM provider and format your own prompts: the official openai or @anthropic-ai/sdk client is smaller, simpler, and has no abstraction tax
- Version alignment annoys you: every @langchain/* package in your app must resolve to the same @langchain/core version, and mismatches produce baffling instanceof and type errors that you fix with npm overrides or pnpm resolutions
- You dislike heavy dependency trees for glue code: core itself pulls in zod, p-queue, mustache, js-tiktoken, langsmith, and more, before you add a single provider
- You are in the Vercel/Next.js world and mainly want streaming UI helpers: the ai package (Vercel AI SDK) is the more native fit there and carries less framework
Setup reality
npm install @langchain/core is easy; keeping it consistent is the real work. Provider packages declare core as a peer dependency, so with multiple @langchain/* packages you can silently end up with two core copies and get errors like a prompt not being accepted as a Runnable. The documented fix is pinning one version via package.json overrides (npm) or resolutions (yarn/pnpm). Node 20+ is required, the package is dual ESM/CJS, and imports come from deep paths like @langchain/core/messages and @langchain/core/prompts, which your editor will not always autocomplete correctly. The v1 release also moved and renamed things, so pre-1.0 tutorials frequently show imports that no longer exist.
Patterns
Build a chat prompt templateprompt-template
import { ChatPromptTemplate } from "@langchain/core/prompts";
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a strict copy editor."],
["human", "Fix this text: {text}"],
]);
const messages = await prompt.invoke({ text: "teh cat sat" });Braces are template variables; to output a literal { or } in the prompt you must double them as {{ and }}.
Pipe prompt, model, and parser into a chainchain-with-pipe
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatOpenAI } from "@langchain/openai";
const chain = ChatPromptTemplate.fromTemplate("Summarize: {input}")
.pipe(new ChatOpenAI({ model: "gpt-4o-mini" }))
.pipe(new StringOutputParser());
const summary = await chain.invoke({ input: longText });The model class comes from a provider package, not core. If pipe complains that the prompt is not a Runnable, you almost certainly have two @langchain/core versions installed.
Stream a chain token by tokenstream-response
const stream = await chain.stream({ input: "Why is the sky blue?" });
for await (const chunk of stream) {
process.stdout.write(chunk);
}Every Runnable exposes stream(); with a StringOutputParser at the end the chunks are plain strings, otherwise they are AIMessageChunk objects you concat yourself.
Create messages directlyconstruct-messages
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
const res = await model.invoke([
new SystemMessage("Answer in one sentence."),
new HumanMessage("What is an LRU cache?"),
]);
console.log(res.content);res.content can be a string or an array of content blocks depending on the provider and modality; check before assuming string.
Define a tool with a zod schemadefine-tool
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const getWeather = tool(
async ({ city }) => `72F and sunny in ${city}`,
{
name: "get_weather",
description: "Get current weather for a city",
schema: z.object({ city: z.string() }),
}
);
const modelWithTools = model.bindTools([getWeather]);bindTools only attaches the schemas; the model returns tool_calls on the AIMessage and executing the tool (or looping) is your job unless you use langgraph agents.
Wrap any function as a Runnablecustom-runnable
import { RunnableLambda } from "@langchain/core/runnables";
const upper = RunnableLambda.from(async (s: string) => s.toUpperCase());
const chain = someChain.pipe(upper);
await upper.invoke("hi"); // "HI"This is how arbitrary business logic joins a chain and inherits invoke, batch, and stream for free.
Run branches in parallel over one inputparallel-branches
import { RunnableMap } from "@langchain/core/runnables";
const combined = RunnableMap.from({
summary: summarizeChain,
tags: tagChain,
});
const { summary, tags } = await combined.invoke({ input: text });Branches run concurrently and the result is an object with the same keys; this is also the standard trick for feeding retriever context plus the raw question into a prompt.
Process many inputs with bounded concurrencybatch-inputs
const results = await chain.batch(
[{ input: a }, { input: b }, { input: c }],
{ maxConcurrency: 2 }
);batch preserves input order in its results. Set maxConcurrency or you will discover your provider's rate limits the hard way.
Add retry and a fallback modelretries-and-fallbacks
const reliable = primaryModel
.withRetry({ stopAfterAttempt: 3 })
.withFallbacks([backupModel]);
const res = await reliable.invoke(messages);withRetry retries the same Runnable on error; withFallbacks moves to the next one after the first fails, and both return a new Runnable rather than mutating the original.
Get typed structured output from a modelstructured-output
import { z } from "zod";
const schema = z.object({
sentiment: z.enum(["pos", "neg", "neutral"]),
confidence: z.number(),
});
const structured = model.withStructuredOutput(schema);
const out = await structured.invoke("I love this library");
// out.sentiment, out.confidence are typedwithStructuredOutput is defined on BaseChatModel in core but relies on the provider supporting tool calling or JSON mode; not every model can do it.
Force a single @langchain/core across packagespin-core-version
// package.json (npm)
{
"overrides": {
"@langchain/core": "1.2.4"
}
}
// yarn or pnpm use "resolutions" insteadAll @langchain/* packages must share one core instance or instanceof checks fail at runtime. Check with: npm ls @langchain/core.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| ai | npm | You are building a TypeScript or Next.js app and want streaming, tool calls, and UI hooks with a lighter abstraction than LangChain. |
| openai | npm | You only target OpenAI-compatible APIs and prefer a direct SDK with no framework layer. |
| llamaindex | npm | Your app is retrieval-heavy and you want indexes and query engines as the primary abstraction. |