Sameer Singh

If you have been learning LangChain for any length of time, you have almost certainly hit a wall.
You understand the basics. You know what a PromptTemplate does. You have called an LLM. You have even parsed some output. But then you open a modern LangChain tutorial or documentation page, and suddenly you are staring at things like:
RunnableSequenceRunnableParallelRunnablePassthroughRunnableLambdaRunnableBranch| connecting everything togetherAnd the question hits: What is all of this?
This is not a small gap in your knowledge. This is actually the most important architectural shift LangChain has made, and once you understand it, everything else clicks into place. Runnables are not just a feature. They are the foundation of modern LangChain.
In this guide, we will build your understanding from the ground up. We will cover:
Let us get started.
In the early days of LangChain, building an application meant wiring together a handful of core components:
PromptTemplate to format user input into a structured promptLLM or ChatModel to send that prompt to a language modelOutputParser to clean up and structure the model's responseRetriever to fetch relevant documents from a vector storeMemory to maintain conversation history across turnsThese components worked, but they each had their own method signatures. There was no shared interface. To run each one, you had to know its specific API:
# Every component had a completely different method
formatted = prompt.format(topic="AI") # PromptTemplate
response = llm.predict(formatted) # LLM
parsed = parser.parse(response) # OutputParser
docs = retriever.get_relevant_documents(query) # RetrieverThis is fine when you have three or four components. But as applications grew more complex, the lack of a unified interface became a serious bottleneck.
LangChain's initial answer was to introduce Chains. Rather than manually calling each component, you could use a pre-built Chain that connected them:
LLMChain for prompt-plus-LLM workflowsSequentialChain for running steps in orderRetrievalQAChain for question answering over documentsAPIChain for calling external APIsSQLChain for natural language database queriesThis helped, but it introduced a different problem: too much fragmentation.
Every new use case required a new Chain class. The library became increasingly large and hard to navigate. Combining two different types of chains was messy. Maintaining custom chains was painful. New developers had to learn a sprawling collection of classes before building anything meaningful.
What LangChain needed was not more chains. It needed a single, composable interface that everything could share.
A Runnable is any component that follows a single, unified interface:
A Runnable takes input and produces output, and it does so using a consistent set of methods.
That definition sounds deceptively simple, but the implications are enormous. Because every component follows the same interface, any component can be connected to any other component without custom wiring.
The three key methods every Runnable exposes are:
| Method | Description |
|---|---|
| invoke(input) | Run the component on a single input and return the result |
| batch(inputs) | Run the component on a list of inputs in parallel |
| stream(input) | Stream the output token by token (useful for LLMs) |
Here is the simplest possible example:
# Every runnable, regardless of type, can be called this way
result = some_runnable.invoke({"topic": "machine learning"})It does not matter whether some_runnable is a prompt template, a language model, a parser, or a retriever. The call looks identical.
The power of this unified interface becomes obvious when you start connecting components. Because everything speaks the same language, you can compose them freely. The output of one Runnable becomes the input of the next, forming a pipeline.
This is the same principle that makes Unix command-line pipes so powerful: cat file.txt | grep "error" | sort | uniq. Each tool does one thing, uses a common interface (stdin/stdout), and can be composed in any combination.
LangChain Runnables bring this philosophy to AI application development.
When Runnables were introduced, all of LangChain's core components were refactored to implement the Runnable interface:
| Old Component | Now |
|---|---|
| PromptTemplate | Runnable |
| ChatOpenAI (LLM) | Runnable |
| OutputParser | Runnable |
| Retriever | Runnable |
| Tool | Runnable |
Since they all share the same interface, you no longer need specialized Chain classes to connect them. You just compose them directly.
Instead of:
# Old approach: use a specialized Chain class
from langchain.chains import LLMChain
chain = LLMChain(llm=llm, prompt=prompt)
result = chain.run("Tell me a joke about AI")You write:
# New approach: compose Runnables directly
chain = prompt | llm | parser
result = chain.invoke({"topic": "AI"})The result is the same. But the new approach is simpler, more readable, more flexible, and easier to extend.
LangChain's Runnables fall into two broad categories.
These are the components that perform the actual processing in your application:
PromptTemplate / ChatPromptTemplate: Formats user input into a structured promptChatOpenAI / ChatAnthropic: Calls the language model and returns a responseStrOutputParser / JsonOutputParser: Parses and structures the model's outputVectorStoreRetriever: Fetches relevant documents from a vector storeEach of these does a specific job. They are the workers in your pipeline.
These are the components that determine how task-specific Runnables are connected and executed:
RunnableSequence: Runs Runnables one after anotherRunnableParallel: Runs multiple Runnables simultaneously on the same inputRunnablePassthrough: Passes input through unchangedRunnableLambda: Wraps a plain Python function as a RunnableRunnableBranch: Adds conditional if/else logic to a pipelineThe rest of this guide explores each primitive in depth.
RunnableSequence is the most fundamental primitive. It connects Runnables in a linear chain, where the output of one step becomes the input of the next.
When to use it: Any time you need to process input through multiple steps in order.
Example: A Basic Joke Generator
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableSequence
# Define each component
prompt = ChatPromptTemplate.from_template("Tell me a short joke about {topic}.")
model = ChatOpenAI(model="gpt-4o")
parser = StrOutputParser()
# Connect them into a sequence
chain = RunnableSequence(prompt, model, parser)
# Run the chain
result = chain.invoke({"topic": "machine learning"})
print(result)
# Output: Why did the neural network break up with the dataset? Too many issues to train on.How data flows through the sequence:
Input: {"topic": "machine learning"}
|
v
PromptTemplate --> "Tell me a short joke about machine learning."
|
v
ChatOpenAI --> AIMessage("Why did the neural network...")
|
v
StrOutputParser --> "Why did the neural network..."
|
v
OutputWriting RunnableSequence(prompt, model, parser) works, but LangChain provides an even cleaner way to express the same thing using the pipe operator |. This is called LangChain Expression Language (LCEL).
# These two are exactly equivalent
chain = RunnableSequence(prompt, model, parser)
chain = prompt | model | parserThe pipe syntax is now the standard in modern LangChain. It is shorter, more readable, and makes the flow of data visually obvious. You will see it everywhere in official documentation and production codebases.
You can also chain operators together with other structures:
# Multi-step pipeline with preprocessing and postprocessing
full_chain = preprocess_fn | prompt | model | parser | postprocess_fnRunnableParallel takes a single input and passes it to multiple Runnables simultaneously. It collects all the outputs into a single dictionary.
When to use it: When you need to generate multiple independent outputs from the same input, without waiting for one to finish before starting the next.
Example: Generating Content for Multiple Platforms
from langchain_core.runnables import RunnableParallel
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
model = ChatOpenAI(model="gpt-4o")
parser = StrOutputParser()
# Two different prompts for two different outputs
tweet_prompt = ChatPromptTemplate.from_template(
"Write a punchy tweet about {topic} in under 280 characters."
)
linkedin_prompt = ChatPromptTemplate.from_template(
"Write a professional LinkedIn post about {topic} with 3 key insights."
)
# Build both chains
tweet_chain = tweet_prompt | model | parser
linkedin_chain = linkedin_prompt | model | parser
# Run them in parallel
parallel_chain = RunnableParallel(
tweet=tweet_chain,
linkedin=linkedin_chain
)
result = parallel_chain.invoke({"topic": "the future of AI agents"})
print("Tweet:")
print(result["tweet"])
print("\nLinkedIn Post:")
print(result["linkedin"])Key rules for RunnableParallel:
Combining parallel and sequential: You can nest RunnableParallel inside a RunnableSequence to build sophisticated workflows:
# Generate content for multiple platforms, then score each one
scoring_chain = score_prompt | model | parser
full_pipeline = (
parallel_chain # Generate tweet and linkedin post
| scoring_chain # Score the combined output
)RunnablePassthrough does exactly what its name says: it receives input and returns it unchanged. This sounds trivial, but it is remarkably useful in practice.
When to use it: When you need to carry an original value forward through a pipeline that would otherwise discard it.
The problem it solves:
Imagine a pipeline where you generate a joke and then want to return both the original topic AND the joke. Without RunnablePassthrough, the topic gets discarded as soon as it flows into the prompt:
# Without RunnablePassthrough: original topic is lost
chain = prompt | model | parser
result = chain.invoke({"topic": "AI"})
# result = "Why did the AI go to school?..."
# The original topic {"topic": "AI"} is goneWith RunnablePassthrough:
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
chain = RunnableParallel(
original_input=RunnablePassthrough(), # Pass original input through
joke=(prompt | model | parser) # Also generate the joke
)
result = chain.invoke({"topic": "AI"})
print(result["original_input"]) # {"topic": "AI"}
print(result["joke"]) # "Why did the AI go to school?..."RunnablePassthrough is especially common in RAG (Retrieval-Augmented Generation) pipelines, where you need to pass the original user question through to the final answer while also using it to retrieve documents.
from langchain_core.runnables import RunnablePassthrough
rag_chain = (
RunnableParallel(
context=retriever, # Fetch relevant docs
question=RunnablePassthrough() # Keep original question
)
| answer_prompt
| model
| parser
)RunnableLambda wraps any plain Python function so it can participate in a Runnable pipeline. This is the primary extension point for custom logic.
When to use it:
Example: Counting Words in LLM Output
from langchain_core.runnables import RunnableLambda
def count_words(text: str) -> dict:
word_count = len(text.split())
return {"text": text, "word_count": word_count}
word_counter = RunnableLambda(count_words)
# Now it plugs directly into a pipeline
chain = prompt | model | parser | word_counter
result = chain.invoke({"topic": "neural networks"})
print(result["text"]) # The joke text
print(result["word_count"]) # e.g., 23Example: Preprocessing User Input
def clean_input(user_input: str) -> dict:
"""Normalize input before sending to the prompt."""
return {
"topic": user_input.strip().lower(),
"word_limit": 100
}
cleaner = RunnableLambda(clean_input)
chain = cleaner | prompt | model | parser
result = chain.invoke(" Machine Learning ")Using lambda functions directly:
For simple one-liners, you can pass a lambda directly:
uppercase = RunnableLambda(lambda text: text.upper())
chain = prompt | model | parser | uppercaseRunnableBranch gives you if/else control flow inside a Runnable pipeline. It evaluates a series of conditions against the input and routes to the first matching branch. If no condition matches, it falls back to a default.
When to use it:
Structure of RunnableBranch:
from langchain_core.runnables import RunnableBranch
branch = RunnableBranch(
(condition_1, runnable_if_condition_1_is_true),
(condition_2, runnable_if_condition_2_is_true),
default_runnable # Runs if no condition matches
)Example: Summarize Long Outputs, Pass Through Short Ones
from langchain_core.runnables import RunnableBranch, RunnablePassthrough
# Define a summarization chain
summary_prompt = ChatPromptTemplate.from_template(
"Summarize the following in 2 sentences:\n\n{text}"
)
summarize = summary_prompt | model | parser
# Branch: if output > 200 words, summarize; otherwise pass through
branch = RunnableBranch(
(
lambda text: len(text.split()) > 200,
summarize
),
RunnablePassthrough() # Default: return as-is
)
# Full pipeline
chain = (
prompt
| model
| parser
| branch
)
result = chain.invoke({"topic": "the history of computing"})
print(result)Example: Language-Based Routing
def detect_language(text: str) -> str:
# In practice, you might use a real language detection library
if any(c > "\u0900" for c in text):
return "hindi"
return "english"
language_detector = RunnableLambda(detect_language)
branch = RunnableBranch(
(lambda lang: lang == "hindi", hindi_response_chain),
(lambda lang: lang == "english", english_response_chain),
default_chain
)
router = language_detector | branchLCEL (LangChain Expression Language) is the official name for the pipe-based syntax used to compose Runnables. It was introduced to make Runnable composition as readable and concise as possible.
At its core, LCEL is just syntactic sugar: A | B is equivalent to RunnableSequence(A, B). But the impact on readability is significant, especially for complex pipelines.
Simple sequential pipeline:
chain = prompt | model | parserPipeline with parallel branches:
chain = (
RunnableParallel(
answer=(answer_prompt | model | parser),
source=RunnablePassthrough()
)
)Multi-stage pipeline combining everything:
from langchain_core.runnables import (
RunnableParallel, RunnablePassthrough,
RunnableLambda, RunnableBranch
)
def preprocess(query: str) -> dict:
return {"topic": query.strip(), "length": len(query)}
preprocess_step = RunnableLambda(preprocess)
generate_step = RunnableParallel(
tweet=(tweet_prompt | model | parser),
blog=(blog_prompt | model | parser)
)
validate_step = RunnableBranch(
(lambda x: len(x["tweet"]) > 280, fix_tweet_chain),
RunnablePassthrough()
)
full_pipeline = preprocess_step | generate_step | validate_stepLCEL is not just about aesthetics. Because the pipe syntax creates proper RunnableSequence objects under the hood, you automatically get:
.stream() on any LCEL chain to get token-by-token output from the LLM, even through parsers and post-processors.batch(["input1", "input2"]) to process multiple inputs efficiently with built-in parallelismainvoke(), astream(), and abatch() for async applications# Streaming with LCEL
chain = prompt | model | parser
for chunk in chain.stream({"topic": "black holes"}):
print(chunk, end="", flush=True)
# Batch processing
results = chain.batch([
{"topic": "AI"},
{"topic": "quantum computing"},
{"topic": "renewable energy"}
])
# Async
import asyncio
result = asyncio.run(chain.ainvoke({"topic": "AI"}))This is one of the most common production patterns in LangChain. It uses RunnablePassthrough to carry the original question alongside retrieved context.
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
rag_prompt = ChatPromptTemplate.from_template("""
Answer the question based only on the following context:
Context: {context}
Question: {question}
""")
# Retrieve docs and format them
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
retriever_chain = retriever | RunnableLambda(format_docs)
rag_chain = (
RunnableParallel(
context=retriever_chain,
question=RunnablePassthrough()
)
| rag_prompt
| model
| parser
)
answer = rag_chain.invoke("What is the capital of France?")# Step 1: Generate an outline
outline_chain = outline_prompt | model | parser
# Step 2: Expand each section (in parallel)
section_chain = RunnableParallel(
intro=(intro_prompt | model | parser),
body=(body_prompt | model | parser),
conclusion=(conclusion_prompt | model | parser)
)
# Step 3: Combine and format
def combine_sections(sections: dict) -> str:
return f"{sections['intro']}\n\n{sections['body']}\n\n{sections['conclusion']}"
combine = RunnableLambda(combine_sections)
# Full pipeline
article_pipeline = outline_chain | section_chain | combine# Classify the user intent first
classify_prompt = ChatPromptTemplate.from_template(
"Classify this query as 'factual', 'creative', or 'analytical': {query}"
)
classifier = classify_prompt | model | parser
# Different chains for different query types
factual_chain = factual_prompt | model | parser
creative_chain = creative_prompt | model | parser
analytical_chain = analytical_prompt | model | parser
router = RunnableBranch(
(lambda intent: intent == "factual", factual_chain),
(lambda intent: intent == "creative", creative_chain),
(lambda intent: intent == "analytical", analytical_chain),
factual_chain # default
)
agent_pipeline = classifier | router
result = agent_pipeline.invoke({"query": "Explain how solar panels work"})The best way to think about LangChain Runnables is as LEGO bricks.
Each brick (Runnable) has a standard connection mechanism. It does not matter whether the brick is a prompt, an LLM, a parser, or a custom function. They all connect using the same interface. You snap them together in any configuration you want, and you get a working system.
Before Runnables, LangChain was more like a collection of specialized, proprietary toys. Each one required custom assembly instructions. Runnables replaced that with a universal building system.
| Runnable | Purpose | Common Use Case |
|---|---|---|
| RunnableSequence / | | Linear pipeline | prompt > LLM > parser |
| RunnableParallel | Multiple outputs from one input | Generate tweet + blog post simultaneously |
| RunnablePassthrough | Forward input unchanged | Preserve original question in RAG |
| RunnableLambda | Wrap a Python function | Custom preprocessing or postprocessing |
| RunnableBranch | Conditional routing | Route based on output length or classification |
invoke, batch, and stream methods.LangChain Runnables are not an advanced topic you can safely skip. They are the architectural foundation that everything else in modern LangChain is built on. Once you understand the unified interface, the pipe syntax, and the five primitive types, the entire library becomes dramatically easier to navigate.
Start small. Build a prompt | model | parser chain. Add a RunnableLambda for custom post-processing. Try a RunnableParallel to generate two outputs at once. Before long, you will be composing sophisticated multi-step pipelines with confidence.
The mental shift from "which Chain class do I need?" to "what Runnables should I compose?" is the shift from beginner to fluent LangChain developer. Make that shift, and LangChain becomes a genuinely powerful tool for building production-grade AI applications.
What if you could find the majority element without counting anything? The Boyer-Moore Voting Algorithm does exactly that, in O(n) time and O(1) space. Here is the full breakdown.
Rahul Kumar
Low Level Design is one of the most important skills for modern software engineers. This detailed guide explains design patterns, UML relationships, aggregation vs composition, object interactions, interview strategies, and how to think like a senior engineer while designing scalable systems.
Sign in to join the discussion.
Rahul Kumar