This is a practical LangChain cheat sheet for Node.js/TypeScript developers. Bookmark it and come back whenever you forget a LangChain function or method.
It covers LangChain.js 1.x, the version that is current in September 2026. Every example is TypeScript. The models come from OpenAI. The vector database is PostgreSQL with the pgvector extension. The example data is the same small set of home notes from my RAG post: bin days, the boiler, the bike.
Each entry has the same shape. A short working example. What it does. When to use it. The parameters that matter. If you want the long version of any piece, the RAG post walks through it. This page is only the reference.
Install and configure
Packages
| Package | What you get |
|---|---|
langchain | createAgent, tool, initChatModel, message classes, middleware |
@langchain/core | Prompt templates, runnables, output parsers, Document |
@langchain/openai | ChatOpenAI, OpenAIEmbeddings |
@langchain/pgvector | PGVectorStore. It brings the pg driver with it |
@langchain/textsplitters | RecursiveCharacterTextSplitter |
@langchain/langgraph | MemorySaver for agent memory |
zod | Schemas for tools and structured output |
mkdir notes-ai && cd notes-ai
npm init -y
npm pkg set type=module
npm install langchain @langchain/core @langchain/openai @langchain/pgvector @langchain/textsplitters @langchain/langgraph zod
npm install -D @types/node
LangChain 1.x needs Node.js 22 or newer. Node 22.18 and newer also run .ts
files directly, so node app.ts works with no build step. When one .ts
file imports another, keep the .ts in the path: import ... from "./setup.ts".
Keys and connection string
export OPENAI_API_KEY="sk-..."
export DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres"
ChatOpenAI and OpenAIEmbeddings read OPENAI_API_KEY on their own. You
don’t pass it in code. For OpenRouter or any other OpenAI-compatible API, add
apiKey and configuration: { baseURL: "..." } to the constructor. Nothing
else changes.
Where to import from
This is the part I forget most often.
| You want | Import from |
|---|---|
createAgent, tool, initChatModel | langchain |
HumanMessage, AIMessage, SystemMessage, ToolMessage | langchain |
trimMessages, filterMessages | langchain |
summarizationMiddleware, modelRetryMiddleware, and friends | langchain |
ChatPromptTemplate, MessagesPlaceholder, PromptTemplate | @langchain/core/prompts |
RunnableSequence, RunnablePassthrough, RunnableLambda | @langchain/core/runnables |
StringOutputParser | @langchain/core/output_parsers |
Document | @langchain/core/documents |
ChatOpenAI, OpenAIEmbeddings | @langchain/openai |
PGVectorStore | @langchain/pgvector |
RecursiveCharacterTextSplitter | @langchain/textsplitters |
MemorySaver | @langchain/langgraph |
PostgresSaver | @langchain/langgraph-checkpoint-postgres |
TextLoader and other file loaders | @langchain/classic/document_loaders/fs/… |
z | zod |
Shared setup for the examples
The snippets below use these three objects. Save this as setup.ts and
import from it.
// setup.ts
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai";
import { PGVectorStore } from "@langchain/pgvector";
export const chatModel = new ChatOpenAI({ model: "gpt-5.4-mini", temperature: 0 });
export const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" });
export const vectorStore = await PGVectorStore.initialize(embeddings, {
postgresConnectionOptions: { connectionString: process.env.DATABASE_URL },
tableName: "notes",
dimensions: 1536,
});
Model names go out of date. Swap in whatever your provider lists today.
Chat models
new ChatOpenAI()
import { ChatOpenAI } from "@langchain/openai";
const chatModel = new ChatOpenAI({
model: "gpt-5.4-mini",
temperature: 0,
maxTokens: 500,
maxRetries: 2,
});
What it does: Creates a chat model object. It doesn’t call the API yet.
When to use it: Any time you want a model to write something. Make one and share it across your app.
Important parameters: model is the model name. temperature from 0
(steady) to 2 (random). Use 0 for RAG and extraction. maxTokens caps the
length of the reply. maxRetries is how often to retry a failed call, 6 by
default. apiKey and configuration.baseURL are for other providers.
initChatModel()
import { initChatModel } from "langchain";
const chatModel = await initChatModel("openai:gpt-5.4-mini", { temperature: 0 });
What it does: Builds a chat model from a provider:model string.
When to use it: When the model name comes from config or an environment variable and you don’t want to import a provider class. The provider package still has to be installed.
Important parameters: The string. The second argument takes the same options as the provider class.
chatModel.invoke()
import { chatModel } from "./setup.ts";
const reply = await chatModel.invoke([
{ role: "system", content: "Answer in one short sentence." },
{ role: "user", content: "What does pgvector do?" },
]);
console.log(reply.text);
What it does: Sends messages to the model and waits for the whole reply.
The reply is an AIMessage.
When to use it: The normal way to call a model when you don’t need streaming.
Important parameters: A plain string, an array of messages, or a prompt
value. The second argument is an options object. signal, tags and
callbacks go there.
chatModel.batch()
const replies = await chatModel.batch(
["What does pgvector do?", "What is an embedding?"],
{ maxConcurrency: 2 },
);
console.log(replies.map((reply) => reply.text));
What it does: Runs invoke for each input in parallel and returns the
replies in the same order.
When to use it: Many independent calls, like summarising every note in a folder.
Important parameters: The array of inputs. maxConcurrency limits how
many run at once, which keeps you under rate limits.
Messages
Message classes
import { SystemMessage, HumanMessage, type BaseMessage } from "langchain";
import { chatModel } from "./setup.ts";
const messages: BaseMessage[] = [
new SystemMessage("You are a helpful assistant."),
new HumanMessage("What does pgvector do?"),
];
const reply = await chatModel.invoke(messages); // an AIMessage
messages.push(reply);
What it does: Each class is one turn of a conversation. SystemMessage
sets the rules. HumanMessage is the user. AIMessage is the model.
ToolMessage is the result of a tool call.
When to use it: When you keep a conversation in an array. The
{ role: "user", content: "..." } shorthand works too, and LangChain converts
it for you.
Reading a reply
const reply = await chatModel.invoke("Which day is paper collected?");
reply.text; // the answer as plain text
reply.content; // raw content: a string or an array of blocks
reply.tool_calls; // tools the model wants to call, if you bound any
reply.usage_metadata; // { input_tokens, output_tokens, total_tokens }
reply.response_metadata; // provider details, such as the exact model name
What it does: These are the fields on an AIMessage you’ll actually read.
When to use it: text almost always. usage_metadata for cost logging.
tool_calls when you run the tool loop yourself.
trimMessages()
import { trimMessages } from "langchain";
const recentMessages = await trimMessages(messages, {
maxTokens: 10,
strategy: "last",
startOn: "human",
tokenCounter: (messagesToCount) => messagesToCount.length,
});
What it does: Cuts a long message array down to fit a budget.
When to use it: Before sending a long chat history to the model.
Important parameters: maxTokens is the budget. tokenCounter decides
what a “token” is. The one above counts messages, so this keeps the last 10.
Pass the model itself as tokenCounter to count real tokens. strategy: "last" keeps the newest messages. startOn: "human" makes sure the result
starts with a user turn, which some providers require.
Prompts and prompt templates
ChatPromptTemplate.fromMessages()
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { chatModel } from "./setup.ts";
const prompt = ChatPromptTemplate.fromMessages([
["system", "Answer using only these notes:\n\n{context}"],
["human", "{question}"],
]);
const promptValue = await prompt.invoke({
context: "General waste goes out every Friday.",
question: "Which bin goes out on Friday?",
});
const reply = await chatModel.invoke(promptValue);
What it does: A prompt with holes in it. {context} and {question} get
filled in when you call invoke. The result can go straight into a model.
When to use it: Any prompt you send more than once. Keep the wording in one place and pass in the parts that change.
Important parameters: An array of [role, text] pairs. Roles are
system, human, ai and placeholder. Only the template text is parsed
for braces, so values can contain { safely. For a literal brace in the
template itself, write {{.
MessagesPlaceholder
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant."],
new MessagesPlaceholder("history"),
["human", "{question}"],
]);
const promptValue = await prompt.invoke({
history: previousMessages,
question: "And which bin goes out on Monday?",
});
What it does: A slot that takes a whole array of messages instead of one string.
When to use it: Chat history. See the memory section below.
Important parameters: The variable name. ["placeholder", "{history}"]
is the same thing in tuple form.
PromptTemplate.fromTemplate()
import { PromptTemplate } from "@langchain/core/prompts";
const prompt = PromptTemplate.fromTemplate("Rewrite this note in one line:\n\n{note}");
const text = await prompt.format({ note: "The boiler resets with the black button." });
What it does: The same idea for a single string instead of a list of messages.
When to use it: Rarely with chat models. It’s handy when you need the filled-in string itself, for logging or for a non-chat API.
Chains
.pipe()
import { StringOutputParser } from "@langchain/core/output_parsers";
const chain = prompt.pipe(chatModel).pipe(new StringOutputParser());
const answer = await chain.invoke({
context: "General waste goes out every Friday.",
question: "Which bin goes out on Friday?",
});
console.log(answer); // a plain string
What it does: Joins steps so the output of one is the input of the next.
Prompt, model, and parser are all “runnables”, so they all have invoke,
stream and batch, and so does the chain.
When to use it: Whenever you’d otherwise write three awaits in a row. The chain can be streamed as one thing.
StringOutputParser
const parser = new StringOutputParser();
const text = await parser.invoke(reply); // AIMessage -> string
What it does: Pulls the text out of an AIMessage.
When to use it: As the last step of a chain, so callers get strings instead of message objects.
RunnableSequence.from()
import { RunnableSequence } from "@langchain/core/runnables";
const chain = RunnableSequence.from([prompt, chatModel, new StringOutputParser()]);
What it does: The same chain as a row of pipes, written as a list.
When to use it: Longer chains, or when the first step is an object that builds the input. The RAG section has that case.
Embeddings
new OpenAIEmbeddings()
import { OpenAIEmbeddings } from "@langchain/openai";
const embeddings = new OpenAIEmbeddings({
model: "text-embedding-3-small",
dimensions: 1536,
});
What it does: Creates the object that turns text into vectors, which are lists of numbers that stand for meaning.
When to use it: Once. Hand it to the vector store, which calls it for you.
Important parameters: model. dimensions sets the vector length. The
text-embedding-3 models accept smaller values like 512. Whatever you pick
has to match the dimensions of your vector store table.
embedQuery() and embedDocuments()
const questionVector = await embeddings.embedQuery("Which bin goes out on Friday?");
const noteVectors = await embeddings.embedDocuments([
"Paper is collected on the first Monday of the month.",
"General waste goes out every Friday.",
]);
console.log(questionVector.length, noteVectors.length); // 1536 2
What it does: embedQuery embeds one string. embedDocuments embeds an
array, in batches.
When to use it: Almost never directly. PGVectorStore calls
embedDocuments when you add documents and embedQuery when you search.
Call them yourself only when you write your own SQL.
Documents
new Document()
import { Document } from "@langchain/core/documents";
const noteDocument = new Document({
pageContent: "General waste goes out every Friday. The bin store code is 4417.",
metadata: { source: "flat.md", topic: "bins" },
});
What it does: The unit LangChain passes around. Some text plus a metadata object. Chunks are documents. Search results are documents.
When to use it: When you build your own documents from a database, an API or a string. Loaders and splitters make them for you otherwise.
Important parameters: pageContent is the text that gets embedded.
metadata is anything you want to filter on or show later. Keep it flat and
small.
Document loaders
import { TextLoader } from "@langchain/classic/document_loaders/fs/text";
const loader = new TextLoader("./notes/flat.md");
const documents = await loader.load(); // Document[] with metadata.source set
What it does: Reads a file into documents.
When to use it: Reading local files. Loaders live in @langchain/classic
now, which is a separate install. For a small project, reading the file with
fs and making a Document yourself is just as good.
Text splitters
new RecursiveCharacterTextSplitter()
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 300,
chunkOverlap: 50,
});
What it does: Cuts long text into chunks. It tries to split on blank lines first, then line breaks, then spaces. So paragraphs stay whole when they fit.
When to use it: Before adding anything longer than a paragraph to a vector store. Small chunks match questions better than whole files.
Important parameters: chunkSize in characters. chunkOverlap is how
much of the end of one chunk repeats at the start of the next. 200 to 500
characters worked for my notes. Go bigger for long documents.
splitText(), createDocuments(), splitDocuments()
const chunks = await splitter.createDocuments(
[flatNoteText, bikeNoteText],
[{ source: "flat.md" }, { source: "bike.md" }],
);
const chunksFromDocuments = await splitter.splitDocuments(documents);
const pieces = await splitter.splitText(flatNoteText); // string[]
What it does: Three ways to run the splitter. createDocuments takes
strings and returns documents. splitDocuments takes documents and returns
smaller documents with the same metadata. splitText returns plain strings.
When to use it: createDocuments when you have strings and want to
attach metadata. splitDocuments after a loader.
Important parameters: For createDocuments, the second array is one
metadata object per text. Every chunk from a text gets a copy, plus a loc
field with the line range it came from.
RecursiveCharacterTextSplitter.fromLanguage()
const markdownSplitter = RecursiveCharacterTextSplitter.fromLanguage("markdown", {
chunkSize: 300,
chunkOverlap: 50,
});
What it does: A splitter with separators that fit a format. For Markdown it splits on headings first.
When to use it: Markdown files or source code. Supported names include
markdown, html, js, python and latex.
PostgreSQL and pgvector
Start Postgres
docker run --name notes-postgres -p 5432:5432 \
-e POSTGRES_PASSWORD=postgres -d pgvector/pgvector:pg17
The pgvector project ships a Postgres image with the extension built in.
PGVectorStore.initialize runs CREATE EXTENSION IF NOT EXISTS vector for
you, so there is nothing else to set up.
PGVectorStore.initialize()
import { PGVectorStore } from "@langchain/pgvector";
import { embeddings } from "./setup.ts";
const vectorStore = await PGVectorStore.initialize(embeddings, {
postgresConnectionOptions: { connectionString: process.env.DATABASE_URL },
tableName: "notes",
columns: {
idColumnName: "id",
contentColumnName: "content",
metadataColumnName: "metadata",
vectorColumnName: "embedding",
},
distanceStrategy: "cosine",
dimensions: 1536,
});
What it does: Opens a connection pool, creates the extension and the table if they are missing, and returns the store.
When to use it: Once at startup. Reuse the store object everywhere.
Important parameters:
| Option | Meaning |
|---|---|
postgresConnectionOptions | Passed to the pg Pool. A connectionString is enough |
pool | Use this instead if your app already has a pg pool |
tableName | The table. Add schemaName if it shouldn’t be in public |
columns | Renames the four columns. Defaults are id, content, metadata, vector |
distanceStrategy | "cosine" (default, use this for text), "euclidean" or "innerProduct" |
dimensions | Vector length. Must match the embedding model. Without it you can’t add an HNSW index |
scoreNormalization | "distance" (default, lower is better) or "similarity" (0 to 1, higher is better) |
The table it creates has a uuid id, a text content column, a jsonb
metadata column and a vector(1536) column.
createHnswIndex()
await vectorStore.createHnswIndex({ dimensions: 1536, m: 16, efConstruction: 64 });
What it does: Adds an HNSW index so searches stay fast when the table gets big.
When to use it: Once, after you have more than a few thousand rows. Small tables don’t need it.
Important parameters: dimensions must match the column. m and
efConstruction trade build time for accuracy. The values above are the
usual defaults.
end()
await vectorStore.end();
What it does: Closes the connection pool.
When to use it: At the end of a script. Without it the process hangs because the pool keeps Node alive.
Vector store: adding documents
addDocuments()
import { vectorStore } from "./setup.ts";
await vectorStore.addDocuments(chunks);
// Or with your own ids. They must be uuids with the default table.
await vectorStore.addDocuments(chunks, {
ids: chunks.map(() => crypto.randomUUID()),
});
What it does: Embeds every document with the store’s embeddings object, then inserts the rows.
When to use it: Indexing. Run it once per document, not on every request.
Important parameters: The documents. ids if you want to pick them. There
is no upsert. Adding an id that already exists throws, so delete first, then
add.
PGVectorStore.fromDocuments()
import { PGVectorStore } from "@langchain/pgvector";
import { embeddings } from "./setup.ts";
const vectorStore = await PGVectorStore.fromDocuments(chunks, embeddings, {
postgresConnectionOptions: { connectionString: process.env.DATABASE_URL },
tableName: "notes",
dimensions: 1536,
});
What it does: initialize plus addDocuments in one call.
When to use it: Quick scripts and tests. In an app, keep the two steps apart so startup doesn’t re-index.
delete()
await vectorStore.delete({ filter: { source: "flat.md" } });
await vectorStore.delete({ ids: ["9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"] });
What it does: Removes rows by metadata filter or by id.
When to use it: Re-indexing one file. Delete its old chunks by source,
then add the new ones. That leaves other files alone.
Vector store: similarity search
similaritySearch()
import { vectorStore } from "./setup.ts";
const results = await vectorStore.similaritySearch(
"Which bin goes out on Friday?",
3,
);
for (const result of results) {
console.log(result.metadata.source, "|", result.pageContent);
}
What it does: Embeds the question, then returns the k stored documents
whose vectors are closest to it.
When to use it: Any “find the notes about this” step. It is the heart of RAG.
Important parameters: The first argument is the question as plain text.
The second is k, how many documents to return. 3 to 5 is a good start. The
optional third argument is a metadata filter, shown below.
similaritySearchWithScore()
const scoredResults = await vectorStore.similaritySearchWithScore(
"Which bin goes out on Friday?",
3,
);
for (const [result, distance] of scoredResults) {
console.log(distance.toFixed(2), result.pageContent);
}
What it does: The same search, but each result comes with its distance.
When to use it: Debugging. If the right chunk isn’t in the top three, this tells you how far off it is. Also for dropping weak matches with a cutoff.
Important parameters: Same as similaritySearch. With cosine distance the
number is 0 to 2 and lower is better. Around 0.3 is a good match. Around 1 is
unrelated. Set scoreNormalization: "similarity" on the store if you’d
rather have 0 to 1 with higher as better.
Metadata filters
const results = await vectorStore.similaritySearch(
"Which bin goes out on Friday?",
3,
{ source: "flat.md" },
);
What it does: Adds a WHERE on the metadata column, so only matching
rows are searched.
When to use it: Searching one file, one user’s data, or one date range.
Important parameters: A plain value means equals. These operators also work, and you can mix them:
| Filter | Meaning |
|---|---|
{ source: "flat.md" } | Equal |
{ source: { in: ["a.md", "b.md"] } } | Any of these |
{ source: { notIn: ["a.md"] } } | None of these |
{ source: { neq: "a.md" } } | Not this, or the key is missing |
{ year: { gte: 2024, lt: 2026 } } | Number compare: gt, gte, lt, lte |
{ tags: { arrayContains: ["bike"] } } | A metadata array holds any of these |
Retrievers
asRetriever()
const retriever = vectorStore.asRetriever({ k: 3, filter: { source: "flat.md" } });
What it does: Wraps the store with the search settings baked in.
When to use it: Inside a chain, or when you want the search settings in one place instead of repeated at every call.
Important parameters: k and filter, same as similaritySearch.
searchType: "mmr" with searchKwargs: { fetchK: 10, lambda: 0.5 } fetches
more, then picks results that are relevant but not near copies of each other.
retriever.invoke()
const relevantChunks = await retriever.invoke("Which bin goes out on Friday?");
What it does: Takes a question, returns documents. Same result as
similaritySearch with the baked-in settings.
When to use it: Whenever you have a retriever. It’s a runnable, so it also
works as a step in .pipe() or RunnableSequence.
RAG
RAG as a chain
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";
import type { Document } from "@langchain/core/documents";
import { chatModel, vectorStore } from "./setup.ts";
const retriever = vectorStore.asRetriever({ k: 3 });
const prompt = ChatPromptTemplate.fromMessages([
[
"system",
"Answer using only these notes. If they don't contain the answer, say you don't know.\n\n{context}",
],
["human", "{question}"],
]);
function formatDocuments(documents: Document[]): string {
return documents.map((document) => document.pageContent).join("\n\n");
}
const ragChain = RunnableSequence.from([
{
context: retriever.pipe(formatDocuments),
question: new RunnablePassthrough(),
},
prompt,
chatModel,
new StringOutputParser(),
]);
const answer = await ragChain.invoke("Which bin goes out on Friday?");
console.log(answer);
What it does: One chain that takes a question and returns an answer. The
first step is an object. Each key runs on the question in parallel:
context searches and formats, question passes the string through. Then
prompt, model, parser.
When to use it: Simple question answering over your own data. This is the version I reach for first.
Important parameters: retriever.pipe(formatDocuments) turns the
documents into one string for the prompt. new RunnablePassthrough() hands
the input through unchanged. Swap invoke for stream to get tokens as they
arrive.
RAG as an agent
import { createAgent, tool } from "langchain";
import * as z from "zod";
import { chatModel, vectorStore } from "./setup.ts";
const searchNotes = tool(
async ({ query }) => {
const results = await vectorStore.similaritySearch(query, 3);
return results.map((result) => result.pageContent).join("\n\n");
},
{
name: "search_notes",
description: "Search my personal notes about the flat, the bins and the bike.",
schema: z.object({
query: z.string().describe("What to search for"),
}),
},
);
const agent = createAgent({
model: chatModel,
tools: [searchNotes],
systemPrompt: "Use search_notes to answer. If the notes don't have it, say so.",
});
const result = await agent.invoke({
messages: [{ role: "user", content: "Which bin goes out on Friday?" }],
});
console.log(result.messages.at(-1)?.text);
What it does: The search is a tool. The model decides when to call it, and how many times, and can rewrite the query.
When to use it: When one search isn’t always enough, or the question might not need the notes at all. It costs more calls than the chain.
Important parameters: The tool description is what the model reads to
decide whether to search. Write it plainly. The older
createRetrievalChain helpers now live in @langchain/classic. I wouldn’t
start new code with them.
Tools
tool()
import { tool } from "langchain";
import * as z from "zod";
const getBinDay = tool(
async ({ binType }) => {
const schedule: Record<string, string> = {
paper: "Monday",
plastic: "Tuesday",
general: "Friday",
};
return schedule[binType] ?? "No collection day found for that bin.";
},
{
name: "get_bin_day",
description: "Return the collection day for a bin type: paper, plastic or general.",
schema: z.object({
binType: z.string().describe("The bin type: paper, plastic or general"),
}),
},
);
What it does: Turns a function into something a model can call. The zod schema becomes the argument list the model sees.
When to use it: Any time the model needs to look something up, calculate something or change something. Database queries, HTTP calls, your own functions.
Important parameters: name in snake_case. description says what it
does and when to use it. The model picks tools by reading it. schema
describes the arguments. Add .describe() to each field. Return a string or
a plain object.
myTool.invoke()
const day = await getBinDay.invoke({ binType: "general" });
console.log(day); // "Friday"
What it does: Runs the tool yourself, with no model involved.
When to use it: Testing a tool before giving it to an agent.
chatModel.bindTools()
import { HumanMessage, type BaseMessage } from "langchain";
import { chatModel } from "./setup.ts";
const modelWithTools = chatModel.bindTools([getBinDay]);
const messages: BaseMessage[] = [new HumanMessage("When does the general waste bin go out?")];
const reply = await modelWithTools.invoke(messages);
messages.push(reply);
for (const toolCall of reply.tool_calls ?? []) {
const toolMessage = await getBinDay.invoke(toolCall); // returns a ToolMessage
messages.push(toolMessage);
}
const finalReply = await modelWithTools.invoke(messages);
console.log(finalReply.text);
What it does: Tells the model which tools exist. The model replies with
tool_calls instead of text. You run the tools, add the results as
ToolMessages, and call the model again.
When to use it: When you want to control the loop yourself, for example
to approve each call. For everything else, createAgent does this loop for
you.
Important parameters: The array of tools. Passing a whole toolCall
object to invoke gives you back a ToolMessage with the right
tool_call_id already set.
Agents
createAgent()
import { createAgent } from "langchain";
import { chatModel } from "./setup.ts";
const agent = createAgent({
model: chatModel,
tools: [getBinDay, searchNotes],
systemPrompt: "You help me look after my flat. Keep answers short.",
});
What it does: Builds the tool loop for you. The model picks a tool, the agent runs it, the model sees the result, and this repeats until the model answers in text.
When to use it: Anything with tools. This is the main way to build with LangChain 1.x.
Important parameters:
| Option | Meaning |
|---|---|
model | A model object, or a string like "openai:gpt-5.4-mini" |
tools | The tools it may call |
systemPrompt | The system message |
checkpointer | Saves the conversation between calls. See memory below |
responseFormat | A zod schema for a typed final answer. See structured output below |
middleware | Retries, summarising, limits and other hooks. See below |
contextSchema | A zod schema for per-request data your tools can read from config |
agent.invoke()
const result = await agent.invoke({
messages: [{ role: "user", content: "Which bin goes out on Friday?" }],
});
console.log(result.messages.at(-1)?.text);
What it does: Runs the whole loop and returns the final state.
When to use it: Every agent call. Use agent.stream when you want to
show progress.
Important parameters: An object with messages. The result has
messages with everything that happened: your message, the tool calls, the
tool results, and the final answer last. When a run goes wrong, print the
whole array.
Middleware
import { createAgent, modelRetryMiddleware, summarizationMiddleware } from "langchain";
const agent = createAgent({
model: chatModel,
tools: [searchNotes],
middleware: [
modelRetryMiddleware({ maxRetries: 3 }),
summarizationMiddleware({
model: chatModel,
trigger: { tokens: 4000 },
keep: { messages: 20 },
}),
],
});
What it does: Small plug-ins that run before or after the model and tool calls.
When to use it: Instead of writing your own retry or history-trimming
code. Other built-ins are toolRetryMiddleware, modelFallbackMiddleware,
toolCallLimitMiddleware, humanInTheLoopMiddleware and piiMiddleware.
Structured output
chatModel.withStructuredOutput()
import * as z from "zod";
import { chatModel } from "./setup.ts";
const binScheduleSchema = z.object({
binType: z.string().describe("The kind of bin"),
day: z.string().describe("The collection day"),
});
const structuredModel = chatModel.withStructuredOutput(binScheduleSchema);
const schedule = await structuredModel.invoke(
"The general waste bin goes out every Friday.",
);
console.log(schedule); // { binType: "general waste", day: "Friday" }
What it does: Makes the model return an object that matches the schema, already parsed and typed. No JSON parsing on your side.
When to use it: Extraction, classification, anything you want to save to a database.
Important parameters: The zod schema. .describe() on each field helps
the model fill it in correctly. Use temperature: 0.
createAgent({ responseFormat })
const agent = createAgent({
model: chatModel,
tools: [searchNotes],
responseFormat: binScheduleSchema,
});
const result = await agent.invoke({
messages: [{ role: "user", content: "When does general waste go out?" }],
});
console.log(result.structuredResponse); // { binType: "general waste", day: "Friday" }
What it does: The agent can use its tools, and its final answer comes
back as a typed object in result.structuredResponse.
When to use it: When the answer needs a lookup first and you want it as data, not prose.
Important parameters: A zod schema. LangChain picks the provider’s native
structured output when it can and falls back to a tool call otherwise. Wrap
the schema in toolStrategy(schema) or providerStrategy(schema) from
langchain to force one.
Memory and conversation history
Keep the messages yourself
import { HumanMessage, type BaseMessage } from "langchain";
import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
import { chatModel } from "./setup.ts";
const prompt = ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant."],
new MessagesPlaceholder("history"),
["human", "{question}"],
]);
const history: BaseMessage[] = [];
async function ask(question: string): Promise<string> {
const promptValue = await prompt.invoke({ history, question });
const reply = await chatModel.invoke(promptValue);
history.push(new HumanMessage(question), reply);
return reply.text;
}
await ask("Paper goes out on Monday.");
console.log(await ask("Which day did I just mention?")); // "Monday"
What it does: Models don’t remember anything between calls. You send the past messages every time. This is all “memory” is.
When to use it: A plain chain without tools. Store the array wherever you
like, in a Map per user or in a database table.
MemorySaver and thread_id
import { createAgent } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
import { chatModel } from "./setup.ts";
const agent = createAgent({
model: chatModel,
tools: [],
checkpointer: new MemorySaver(),
});
const threadConfig = { configurable: { thread_id: "sam-kitchen-chat" } };
await agent.invoke(
{ messages: [{ role: "user", content: "My name is Sam." }] },
threadConfig,
);
const result = await agent.invoke(
{ messages: [{ role: "user", content: "What is my name?" }] },
threadConfig,
);
console.log(result.messages.at(-1)?.text); // "Your name is Sam."
What it does: The checkpointer saves the agent’s state after every call.
The thread_id says which conversation to load. You only send the new
message.
When to use it: Any agent that talks to a user more than once.
MemorySaver keeps it in RAM, so it’s gone on restart. Good for development.
Important parameters: checkpointer on the agent. configurable.thread_id
on every call. Forgetting the thread_id is an error once a checkpointer is
set.
PostgresSaver
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);
await checkpointer.setup(); // creates its tables. Run once.
const agent = createAgent({ model: chatModel, tools: [searchNotes], checkpointer });
What it does: The same as MemorySaver, but in Postgres. Conversations
survive restarts and are shared between servers.
When to use it: Production. It can use the same database as your vectors.
Install @langchain/langgraph-checkpoint-postgres first.
Keeping history short
Long histories cost money and eventually hit the context limit. Two options
from above: trimMessages for chains you run yourself, and
summarizationMiddleware for agents. The old BufferMemory and
RunnableWithMessageHistory classes are legacy now. Don’t start new code
with them.
Streaming
chatModel.stream() and chain.stream()
import { chatModel } from "./setup.ts";
const stream = await chatModel.stream("Explain pgvector in two sentences.");
for await (const chunk of stream) {
process.stdout.write(chunk.text);
}
What it does: Returns pieces of the reply as they arrive instead of waiting for the whole thing.
When to use it: Anything a person watches. Same call on a chain. If the
chain ends in StringOutputParser, each chunk is a string.
agent.stream()
import { isAIMessage } from "@langchain/core/messages";
const stream = await agent.stream(
{ messages: [{ role: "user", content: "Which bin goes out on Friday?" }] },
{ streamMode: "messages" },
);
for await (const [messageChunk] of stream) {
if (isAIMessage(messageChunk)) {
process.stdout.write(messageChunk.text);
}
}
What it does: Streams the model’s tokens while the agent runs, including the tokens of the final answer.
When to use it: A chat UI on top of an agent.
Important parameters: streamMode: "messages" gives [chunk, metadata]
pairs. Tool results come through as well, which is why the example keeps
only the model’s chunks. streamMode: "updates" instead gives one object per step,
with the key model or tools, which is handy for a progress log.
agent.streamEvents()
const stream = await agent.streamEvents(
{ messages: [{ role: "user", content: "Which bin goes out on Friday?" }] },
{ version: "v3" },
);
for await (const message of stream.messages) {
for await (const token of message.text) {
process.stdout.write(token);
}
}
What it does: A newer helper that sorts the stream into stream.messages,
stream.toolCalls and stream.output, each one its own iterator.
When to use it: When you want to show tool calls and text separately without sorting chunks yourself.
Common utilities
| Call | What it does |
|---|---|
runnable.withRetry({ stopAfterAttempt: 3 }) | Retry on failure, with backoff |
runnable.withFallbacks({ fallbacks: [otherModel] }) | Try another runnable when this one throws |
runnable.withConfig({ runName: "answer", tags: [] }) | Attach a name and tags. Shows up in traces |
runnable.batch(inputs, { maxConcurrency: 5 }) | Many inputs, limited parallelism |
runnable.invoke(input, { signal }) | Cancel with an AbortSignal |
filterMessages(messages, { includeTypes: ["human"] }) | Keep only some message types |
RunnableLambda.from(myFunction) | Wrap a plain function so it can sit in a chain |
import { ChatOpenAI } from "@langchain/openai";
import { chatModel } from "./setup.ts";
const backupModel = new ChatOpenAI({ model: "gpt-5.5" });
const reliableModel = chatModel
.withRetry({ stopAfterAttempt: 3 })
.withFallbacks({ fallbacks: [backupModel] });
const reply = await reliableModel.invoke("Which bin goes out on Friday?", {
signal: AbortSignal.timeout(30_000),
tags: ["cheat-sheet"],
});
All of these work on models, prompts, retrievers, chains and agents, because they are all runnables.
Error handling and debugging
Catch, time out, and log usage
import { chatModel } from "./setup.ts";
try {
const reply = await chatModel.invoke("Which bin goes out on Friday?", {
signal: AbortSignal.timeout(30_000),
});
console.log(reply.text);
console.log(reply.usage_metadata); // { input_tokens, output_tokens, total_tokens }
} catch (error) {
console.error("Model call failed:", (error as Error).message);
}
What it does: A normal try and catch. signal gives up after 30
seconds. usage_metadata tells you what the call cost.
When to use it: Around every model call in an app. Rate limits and timeouts are normal, not rare.
Retries for agents
import { createAgent, modelRetryMiddleware, toolRetryMiddleware } from "langchain";
const agent = createAgent({
model: chatModel,
tools: [searchNotes],
middleware: [
modelRetryMiddleware({ maxRetries: 3, initialDelayMs: 1000, backoffFactor: 2 }),
toolRetryMiddleware({ maxRetries: 2 }),
],
});
What it does: Retries failed model calls and failed tool calls with growing delays.
When to use it: Any agent that runs unattended.
See every step
import { ConsoleCallbackHandler } from "@langchain/core/tracers/console";
await ragChain.invoke("Which bin goes out on Friday?", {
callbacks: [new ConsoleCallbackHandler()],
});
What it does: Prints each step of the chain, with its input and output, to the console.
When to use it: When a chain gives a strange answer and you want to see the filled-in prompt.
For a proper trace viewer, set two environment variables and every call is recorded in LangSmith:
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY="lsv2_..."
And for agents, result.messages already holds every tool call and result.
Printing it is often enough.
Common mistakes and best practices
These are the ones that caught me.
- Use the same embedding model for adding and searching. The store does this
for you as long as you give it one
embeddingsobject and never change the model without re-indexing. - Set
dimensionsininitializeand keep it equal to what the model returns. Without it the column has no size and HNSW indexes can’t be used. - Call
vectorStore.end()at the end of a script. Otherwise the process never exits. - There is no upsert. Delete by
filterfirst, thenaddDocuments. Re-adding the same chunks without deleting gives you duplicates in every search. - Put anything you’ll want to filter on into
metadatawhen you split. You can’t filter on what isn’t there. - Check your chunks with
similaritySearchWithScorebefore blaming the model. If the right chunk isn’t in the top three, the prompt can’t fix it. - Use
temperature: 0for RAG, extraction and anything with a schema. - Write tool descriptions for the model, not for yourself. Say what the tool
does and when to use it. Use
snake_casenames. - Set
thread_idon every call once an agent has acheckpointer. - A literal brace in a prompt template is
{{. Values are safe as they are. - Old import paths like
langchain/chainsandlangchain/document_loadersare gone in 1.x. They live in@langchain/classicnow. The old.call(),.predict()and.bind()methods were removed. Useinvoke. - Keep keys in environment variables, even in small projects. The constructors read them without any code.
That’s the set I keep coming back to. For the full working files, the RAG post has one for Postgres and the Qdrant post has one for Qdrant.