LangChain.js runnable types are the TypeScript generic signatures that describe what a runnable accepts as input, emits as output, and expects for configuration. The core Runnable<Input, Output, Config> interface makes these three type parameters explicit, letting the compiler verify chain composition before runtime. Understanding these generics is the difference between catching type mismatches at compile time and debugging cryptic errors in production.
The runnable interface in plain terms
Every LangChain.js component that implements the runnable protocol — prompts, models, output parsers, retrievers, and custom functions — conforms to Runnable<Input, Output, Config>. The three type parameters map directly to the three arguments of the core methods: invoke(input, config?), stream(input, config?), and batch(inputs, config?).
interface Runnable<Input, Output, Config extends RunnableConfig = RunnableConfig> {
invoke(input: Input, config?: Config): Promise<Output>;
stream(input: Input, config?: Config): AsyncIterable<Output>;
batch(inputs: Input[], config?: Config): Promise<Output[]>;
// ... other methods
}
Input is what you pass in. Output is what you get back. Config carries callbacks, metadata, tags, and runtime options like maxConcurrency. The default RunnableConfig covers most cases, but you can extend it when your component needs custom configuration.
Why the generics matter for composition
LangChain.js chains runnables together with the pipe operator (|). The type system enforces that the output of the left operand matches the input of the right operand. If you pipe a Runnable<string, string[]> into a Runnable<string[], number>, TypeScript accepts it. If you reverse them, you get a compile error.
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
const prompt = PromptTemplate.fromTemplate("Summarize: {text}");
// PromptTemplate: Runnable<{ text: string }, PromptValue>
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
// ChatOpenAI: Runnable<PromptValue | string | BaseMessage[], AIMessage>
const parser = new StringOutputParser();
// StringOutputParser: Runnable<AIMessage | BaseMessage, string>
const chain = prompt.pipe(model).pipe(parser);
// chain: Runnable<{ text: string }, string>
The inferred type of chain is Runnable<{ text: string }, string>. You cannot accidentally pass a number where { text: string } is expected, and you cannot treat the result as anything but a string.
Input and output types in practice
Most built-in runnables expose narrow, specific types. A prompt template expects an object whose keys match its input variables. A chat model accepts a prompt value, string, or message array and returns an AIMessage. An output parser transforms that message into a string, JSON, or a structured object.
import { JsonOutputParser } from "@langchain/core/output_parsers";
import { z } from "zod";
const schema = z.object({
title: z.string(),
bullets: z.array(z.string()),
});
const parser = new JsonOutputParser({ schema });
// parser: Runnable<AIMessage, { title: string; bullets: string[] }>
When you pipe this parser after a model, the chain output becomes the parsed object. The compiler knows the shape, so autocomplete works on result.title and result.bullets.
Config typing and custom configuration
The third generic, Config, defaults to RunnableConfig. You rarely need to touch it unless you build a custom runnable that requires extra configuration fields. For example, a retriever that accepts a runtime namespace parameter:
import { Runnable, RunnableConfig } from "@langchain/core/runnables";
interface RetrieverConfig extends RunnableConfig {
namespace?: string;
}
class CustomRetriever extends Runnable<string, Document[], RetrieverConfig> {
async invoke(query: string, config?: RetrieverConfig) {
const ns = config?.configurable?.namespace ?? "default";
// fetch documents from ns
return [];
}
}
Downstream consumers can pass { configurable: { namespace: "tenant-42" } } and TypeScript validates the key exists.
Streaming types and async iterables
stream() returns an AsyncIterable<Output>. For token-by-token streaming, Output is often a string chunk. For object streaming, it might be partial objects. The type system does not automatically infer chunk shapes from the final output — you must know what the specific runnable emits.
for await (const chunk of chain.stream({ text: "long document..." })) {
// chunk: string (because StringOutputParser streams string deltas)
process.stdout.write(chunk);
}
If you swap StringOutputParser for a JsonOutputParser, the stream type changes to partial JSON objects. Check the component documentation or hover the stream method in your editor to see the exact chunk type.
Batch and concurrency
batch(inputs, config?) takes an array of inputs and returns a promise of output arrays. The config can include maxConcurrency to limit parallelism. Types remain consistent: Input[] in, Output[] out.
const results = await chain.batch([
{ text: "doc 1" },
{ text: "doc 2" },
{ text: "doc 3" },
], { maxConcurrency: 2 });
// results: string[]
Common misconceptions
Misconception: RunnableSequence is a distinct type
RunnableSequence is an internal class created by the pipe operator. You rarely reference it directly. The public type is always Runnable<Input, Output, Config>. Treat the pipe result as a black-box runnable with inferred generics.
Misconception: You must annotate every chain
Type inference works well for linear chains. Annotate only when you need to expose a public API boundary, enforce a specific interface, or help the compiler with complex branching.
// Helpful annotation at a module boundary
export const summarizationChain: Runnable<{ text: string }, Summary> =
prompt.pipe(model).pipe(parser);
Misconception: Config propagates automatically through pipes
Config does not merge automatically. The composed runnable’s Config defaults to RunnableConfig. If you need a custom config field at the top level, you must cast or wrap:
const chain = prompt.pipe(model).pipe(parser) as Runnable<
{ text: string },
string,
RetrieverConfig
>;
// Now chain.invoke(input, { configurable: { namespace: "x" } }) type-checks
Misconception: All runnables support streaming
Not every runnable implements stream meaningfully. Some fall back to buffering the full result and yielding it once. Check the component’s documentation. If you need true streaming, verify the runnable advertises streaming support.
Debugging type errors
When the compiler complains about a pipe, read the error from left to right. It will tell you which operand’s output fails to match the next operand’s input. Common causes:
- Forgetting that a prompt template expects an object, not a raw string
- Piping a string-output parser into a component that expects a message
- Mixing
BaseMessagearrays withPromptValuewhere a single type is required
Hover each segment in your IDE to see its inferred Runnable<In, Out>. The mismatch usually jumps out.
Practical pattern: typed wrapper functions
If you expose chains to other teams or packages, wrap them in a function that returns a typed runnable. This hides implementation details and locks the contract.
import { Runnable } from "@langchain/core/runnables";
export interface SummarizeInput {
text: string;
maxLength?: number;
}
export interface Summary {
tldr: string;
keyPoints: string[];
}
export function createSummarizationChain(): Runnable<SummarizeInput, Summary> {
const prompt = PromptTemplate.fromTemplate(/* ... */);
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const parser = new JsonOutputParser({ schema: summarySchema });
return prompt.pipe(model).pipe(parser);
}
Consumers import createSummarizationChain and get full type safety without knowing the internal pipe structure.
When to use RunnableLambda for custom logic
RunnableLambda lets you wrap any async function as a runnable with explicit types. Use it for preprocessing, postprocessing, or side effects that don’t fit existing components.
import { RunnableLambda } from "@langchain/core/runnables";
const addTimestamp = new RunnableLambda({
func: async (input: { text: string }) => ({
...input,
timestamp: new Date().toISOString(),
}),
// Optional: name appears in traces
name: "AddTimestamp",
});
// addTimestamp: Runnable<{ text: string }, { text: string; timestamp: string }>
The lambda infers types from the function signature. You can also pass explicit generics if inference falls short.
Interoperability with non-runnable code
You can always escape the runnable system with .invoke() and handle the raw promise. The boundary is explicit:
const result: string = await chain.invoke({ text: "input" });
// result is a plain string, no runnable machinery involved
This is useful when integrating with legacy code, web frameworks, or any context that doesn’t understand the runnable protocol.
Summary
LangChain.js runnable types center on Runnable<Input, Output, Config>. The pipe operator composes them with full type inference, catching mismatches at compile time. Most day-to-day work requires only the first two generics; the third appears when you build custom components with specialized configuration. Annotate at API boundaries, trust inference inside chains, and hover types in your editor when the compiler disagrees with your mental model. The type system is strict because runnable composition is where runtime surprises are most expensive.