LangChain Cheat Sheet for Node.js, TypeScript and pgvector

31 min read
On this page

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

PackageWhat you get
langchaincreateAgent, tool, initChatModel, message classes, middleware
@langchain/corePrompt templates, runnables, output parsers, Document
@langchain/openaiChatOpenAI, OpenAIEmbeddings
@langchain/pgvectorPGVectorStore. It brings the pg driver with it
@langchain/textsplittersRecursiveCharacterTextSplitter
@langchain/langgraphMemorySaver for agent memory
zodSchemas 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 wantImport from
createAgent, tool, initChatModellangchain
HumanMessage, AIMessage, SystemMessage, ToolMessagelangchain
trimMessages, filterMessageslangchain
summarizationMiddleware, modelRetryMiddleware, and friendslangchain
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/…
zzod

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:

OptionMeaning
postgresConnectionOptionsPassed to the pg Pool. A connectionString is enough
poolUse this instead if your app already has a pg pool
tableNameThe table. Add schemaName if it shouldn’t be in public
columnsRenames the four columns. Defaults are id, content, metadata, vector
distanceStrategy"cosine" (default, use this for text), "euclidean" or "innerProduct"
dimensionsVector 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.

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:

FilterMeaning
{ 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:

OptionMeaning
modelA model object, or a string like "openai:gpt-5.4-mini"
toolsThe tools it may call
systemPromptThe system message
checkpointerSaves the conversation between calls. See memory below
responseFormatA zod schema for a typed final answer. See structured output below
middlewareRetries, summarising, limits and other hooks. See below
contextSchemaA 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

CallWhat 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.

  1. Use the same embedding model for adding and searching. The store does this for you as long as you give it one embeddings object and never change the model without re-indexing.
  2. Set dimensions in initialize and keep it equal to what the model returns. Without it the column has no size and HNSW indexes can’t be used.
  3. Call vectorStore.end() at the end of a script. Otherwise the process never exits.
  4. There is no upsert. Delete by filter first, then addDocuments. Re-adding the same chunks without deleting gives you duplicates in every search.
  5. Put anything you’ll want to filter on into metadata when you split. You can’t filter on what isn’t there.
  6. Check your chunks with similaritySearchWithScore before blaming the model. If the right chunk isn’t in the top three, the prompt can’t fix it.
  7. Use temperature: 0 for RAG, extraction and anything with a schema.
  8. Write tool descriptions for the model, not for yourself. Say what the tool does and when to use it. Use snake_case names.
  9. Set thread_id on every call once an agent has a checkpointer.
  10. A literal brace in a prompt template is {{. Values are safe as they are.
  11. Old import paths like langchain/chains and langchain/document_loaders are gone in 1.x. They live in @langchain/classic now. The old .call(), .predict() and .bind() methods were removed. Use invoke.
  12. 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.


Share: