LangChain Chains and Runnables, Explained Simply (With Real Examples)
If you’ve started learning LangChain, chances are you’ve run into a line of code like this and gone “…wait, what?”
chain = prompt | model | parserA pipe symbol between a prompt and a model? It looks weird the first time you see it. But don’t worry — it’s not some secret Unix trick, and it’s definitely not a typo. It’s actually one of the neatest ideas in the whole framework, called the Runnable interface, and once it clicks, it really clicks.
In this post, we’ll build up to that moment step by step. For every important idea, we’ll first explain it properly (the technical version), and then immediately explain it again in plain, everyday language with a simple example — so whether you’re a developer or you’ve never written a line of code, you’ll walk away actually getting it.
Here’s what we’ll cover:
- What is LangChain?
- What is a Chain?
- The problem with the older Chain design
- How the Runnable interface fixes it
- Chain types and Runnable patterns, with lots of code examples
- A simple picture to tie it all together
No prior LangChain experience needed. Basic Python helps, but you don’t need to be a programmer to follow along.
1. What is LangChain?
Say you want to build an app powered by an AI model — GPT, Claude, Gemini, whatever. Sounds simple until you realize a real app usually needs to:
- Send a well-structured prompt to the model
- Maybe pull in some info from the internet or a database first
- Clean up the model’s response into something usable
- Ask a follow-up question based on what it just said
- Remember what the user said earlier in the chat
Try building all of that by hand with raw API calls, and things get messy real fast — trust me.
LangChain is a framework that gives you the building blocks to snap all these pieces together — prompts, models, tools, memory, data sources — into one smooth pipeline, instead of reinventing the wheel every time.
Here’s an easy way to picture it:
LangChain is like a kitchen stocked with ready-made appliances — a blender, an oven, a mixer. You don’t build each appliance from scratch; you just plug them together in whatever order gets your meal done.
2. What is a Chain?
Technical definition
A Chain is a sequence of components where the output of one component becomes the input to the next. Each component performs a specific transformation — building a prompt, calling a model, parsing a response — and the whole sequence together represents one composed, end-to-end operation.
In simple terms
Picture a car factory. Nobody just shouts “build me a car” and one giant machine spits it out. It’s broken into stations:
- Station 1: Attach the frame
- Station 2: Attach the engine
- Station 3: Paint the car
- Station 4: Final inspection
Each station takes whatever the last station handed it, does its own job, and passes the result along. Nobody skips a step, and nobody starts from scratch.
A LangChain Chain works exactly the same way — except instead of car parts, it’s passing around text, prompts, and model responses.
Example: a Recipe Generator
Say you want to build a “Recipe Generator”:
- Step 1: Take an ingredient from the user (e.g., “chicken”)
- Step 2: Build a prompt: “Give me a recipe using chicken”
- Step 3: Send that prompt to the model
- Step 4: Clean up the model’s response into plain text
That entire flow — Step 1 → 2 → 3 → 4 — is a Chain.
Another everyday example: a Translator + Summarizer
Here’s one more, so it really sticks:
- Step 1: User pastes in a paragraph of French text
- Step 2: Prompt: “Translate this to English”
- Step 3: Send to model, get back the English translation
- Step 4: Take that English translation and build a new prompt: “Summarize this in one sentence”
- Step 5: Send to model again, get back a one-line summary
Two AI calls, chained together, where the output of one feeds directly into the next. Still just a Chain.
3. The Problem With the Older Chain Design
Here’s the part most tutorials skip, and it’s actually the key to understanding why Runnables exist at all.
Before the Runnable interface and LCEL (LangChain Expression Language) came along, chains were built using a fixed set of pre-built classes. Each class handled exactly one pattern of composition — nothing more, nothing less. A basic example:
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplateprompt = PromptTemplate(
input_variables=["ingredient"],
template="Give me a simple recipe using {ingredient}"
)chain = LLMChain(llm=model, prompt=prompt)
result = chain.run(ingredient="chicken")
This works fine for one prompt going to one model. The trouble starts the moment you want to do anything slightly more interesting. Let’s go through the three real problems, one at a time — technical version first, then the easy version right after.
Problem 1: Every composition pattern needed its own special class
Technical explanation: Sequential execution of two chains required SimpleSequentialChain. Passing multiple named inputs and outputs between steps required the stricter SequentialChain, which enforced matching input_variables and output_variables lists. Parallel execution had no dedicated abstraction and generally required custom orchestration code outside the chain framework itself. Each of these classes also exposed a different calling convention — .run(), .predict(), .apply() — rather than one consistent method.
In simple terms: every kind of chain arrangement needed its own special box, and every box worked a little differently. It’s like your kitchen appliances each needing a differently shaped plug — the blender has one, the oven another, the mixer a third — and none of them fit the same socket. Annoying, right?
Problem 2: Adding your own custom logic was a whole ordeal
Technical explanation: To insert custom Python logic into a chain — say, transforming text to uppercase between two steps — the standard approach was to subclass the base Chain class, override internal methods such as _call(), and explicitly declare input_keys and output_keys. That's a lot of boilerplate for something conceptually as small as a one-line transformation.
In simple terms: if you wanted to add one tiny custom step, you had to build an entire new formal component just to hold it — kind of like building a whole cabinet just to store one extra spoon.
Problem 3: Chains were fragile when nested inside other chains
Technical explanation: Because every chain type expected different input and output shapes, nesting one chain inside another meant carefully double-checking that variable names lined up exactly at every layer. A mismatch usually surfaced as a confusing runtime KeyError, not a clear early warning.
In simple terms: combining chains was like taping together mismatched Lego bricks — it could be made to work, but a wrong connection often only showed up once the whole thing was already built and you ran it.
Quick example of Problem 3 in action: imagine chaining a translator → summarizer → tone-changer, where each one was a different Chain subclass. If the summarizer expected a key called "text" but the translator's output was actually stored under "translation", you wouldn't find out until you ran the whole thing and got a cryptic error. Nothing warned you ahead of time.
Summary: the older system was basically a pile of differently-shaped puzzle pieces, each one only fitting in specific pre-approved ways. Every new thing you wanted to do — sequencing, parallel steps, custom logic — meant learning yet another class with its own rules.
4. How the Runnable Interface Fixes This
Technical explanation
The Runnable interface, introduced alongside LCEL, defines one single, uniform contract: any component — a prompt template, a model, an output parser, a custom function, or even an entire chain — implements the same interface and exposes the same methods (invoke, batch, stream, plus their async versions). Because every component shares this identical interface, the | operator can compose any two Runnables into a new Runnable, no matter what each one does internally. Composition stops being handled by specialized classes per pattern, and instead becomes one general mechanism that works the same way for sequencing, parallel execution, and custom logic alike.
In simple terms
If literally everything — prompts, models, parsers, your own functions, even whole chains — agrees to follow one simple rule (take an input, spit out an output), then anything can be connected to anything else. No more hunting for the “right” class for your situation. Just one shape, used everywhere, like Lego bricks that all snap together.
Side-by-side: old pain vs. new simplicity (sequencing)
Before Runnables, sequencing two chains needed a dedicated class:
from langchain.chains import SimpleSequentialChainchain = SimpleSequentialChain(chains=[story_chain, summary_chain])
result = chain.run("a robot learning to paint")This looks short, but it only works because both chains happen to have exactly one input and one output. Need anything more complex? You’re stuck with the stricter SequentialChain and its matching input_variables/output_variables lists.
With Runnables (LCEL), the same thing is just… pipes:
full_chain = (
{"story": story_chain}
| summary_prompt
| model
| parser
)In simple terms: no special “sequential chain” class needed, no list of input/output names that has to match perfectly. One Runnable’s output just flows into the next Runnable — same rule, no matter how you arrange the pieces.
Side-by-side: old pain vs. new simplicity (custom logic)
Before Runnables, adding your own function meant subclassing:
from langchain.chains.base import Chainclass UppercaseChain(Chain):
# ...override input_keys, output_keys, _call(), etc.
passWith Runnables, the same thing takes one line:
uppercase_step = RunnableLambda(lambda text: text.upper())
chain = prompt | model | parser | uppercase_stepIn simple terms: instead of building a whole new formal component just to hold one tiny piece of logic, you wrap your function once and it slots right in like every other piece of the chain.
Summary: the Runnable interface swapped out a pile of special-case classes for one predictable, universal shape that works for sequencing, parallel execution, custom code, and branching — all of it.
So what exactly is a “Runnable”?
Technical explanation: a Runnable is any object that implements the standard Runnable interface — meaning it accepts a defined input, performs its transformation, and produces a defined output, while exposing a consistent set of methods (invoke, batch, stream). Prompt templates, chat models, output parsers, custom functions wrapped in RunnableLambda, and fully composed chains are all Runnables.
In simple terms: a Runnable is anything that takes an input, does its thing, and produces an output. That’s it, genuinely.
A prompt template? Runnable. A model? Runnable. An output parser? Runnable. A whole chain? Also Runnable.
Why this matters
Because every single piece plays by the same “input in, output out” rule, LangChain lets you connect any of them using one simple operator: the pipe |.
Understanding the pipe operator
Technical explanation: the | operator is overloaded on the Runnable class so that a | b returns a new RunnableSequence, where a's output becomes b's input at execution time. That's why this line is valid:
chain = prompt | model | parserIn simple terms: you can read that line exactly like this:
prompt → model → parser“Take the prompt, send its output into the model, take the model’s output, send it into the parser.” That’s genuinely the whole trick. No wizardry — it just means “pass this output to the next thing.”
Analogy: connected pipes
Picture actual water pipes connected end to end:
- Pipe 1 (prompt) shapes the input into a specific format
- Pipe 2 (model) processes that formatted input
- Pipe 3 (parser) filters the result into a clean, final answer
Water (data) flows through automatically. You connect the pipes once, and every time something new goes in, it flows through all of them in order.
[User Input] → [Prompt] → [Model] → [Output Parser] → [Final Answer]This is exactly why modern LangChain code looks so much cleaner than the old Chain-based approach.
The 3 things every Runnable can do
Technical explanation: every Runnable exposes the same three primary execution methods, no matter what it wraps internally:
Get Animesh’s stories in your inbox
Join Medium for free to get updates from this writer.
Method Behavior .invoke() Executes the Runnable once on a single input and returns a single output .batch() Executes the Runnable across multiple inputs, typically with internal parallelization .stream() Executes the Runnable and yields output incrementally as it becomes available
In simple terms:
Method What it does Everyday analogy .invoke() Run once, get one answer Ordering one coffee .batch() Run on multiple inputs at once Ordering ten coffees in one trip .stream() Get the answer piece by piece as it's generated Watching the coffee machine pour in real time instead of waiting for the full cup
This consistency is really the whole point of the Runnable design — learn these 3 methods once, and they work on everything.
One more way to see it click
Here’s the moment it really clicked for me: watch how .invoke() behaves identically whether you call it on a prompt, a model, or a full chain — because they're all Runnables:
# Calling .invoke() on just the prompt
prompt.invoke({"ingredient": "chicken"})
# Output: a formatted prompt, ready for the model# Calling .invoke() on just the model
model.invoke("Give me a recipe using chicken")
# Output: the AI's raw response# Calling .invoke() on the WHOLE chain
chain.invoke({"ingredient": "chicken"})
# Output: the final, parsed recipe text
Same method name. Same pattern. Doesn’t matter if it’s one tiny piece or the whole pipeline — because a Chain is a Runnable too. Once this clicks, LangChain stops feeling like memorizing a hundred class names and starts feeling like snapping Lego bricks together.
5. Chain Types and Runnable Patterns: Code Examples
Okay, theory’s done — let’s build some stuff. Install what you need first:
pip install langchain langchain-openaiExample 1: The most basic chain (Prompt → Model → Parser)
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser# Step 1: The prompt template (a Runnable)
prompt = ChatPromptTemplate.from_template(
"Give me a simple recipe using {ingredient}"
)# Step 2: The AI model (a Runnable)
model = ChatOpenAI(model="gpt-4o-mini")# Step 3: A parser to clean up the output (a Runnable)
parser = StrOutputParser()# Chain them together using the pipe
chain = prompt | model | parser# Run it
result = chain.invoke({"ingredient": "chicken"})
print(result)
What’s happening step by step:
prompttakes{"ingredient": "chicken"}and turns it into: "Give me a simple recipe using chicken"- That text flows into
model, which sends it to the AI and gets a response - That response flows into
parser, which strips it down to plain text .invoke()triggers the whole pipeline in one go
This is called LCEL — LangChain Expression Language — the modern way of building chains using the | symbol.
Example 2: A chain with multiple variables
prompt = ChatPromptTemplate.from_template(
"Write a {tone} birthday message for {name}"
)chain = prompt | model | parserresult = chain.invoke({"tone": "funny", "name": "Raj"})
print(result)
The same pattern applies — just more blanks to fill in.
Example 2b: The Translator + Summarizer from earlier, as real code
Remember our “translate then summarize” example from Section 2? Here’s what it actually looks like:
translate_prompt = ChatPromptTemplate.from_template(
"Translate this text to English: {text}"
)
translate_chain = translate_prompt | model | parsersummarize_prompt = ChatPromptTemplate.from_template(
"Summarize this in one sentence: {translation}"
)full_chain = (
{"translation": translate_chain}
| summarize_prompt
| model
| parser
)result = full_chain.invoke({"text": "Le chat noir dort sur le canapé toute la journée."})
print(result)
The French text goes in, gets translated, and that translation immediately becomes the input for the summarizer — two AI calls, one clean pipeline.
Example 3: Sequential Chain (feeding one chain’s output into another)
This is the “assembly line” idea from earlier, but with two separate AI calls.
Goal: first generate a story, then summarize that story in one line.
from langchain_core.runnables import RunnablePassthrough# Chain 1: Generate a short story
story_prompt = ChatPromptTemplate.from_template(
"Write a 3-line story about {topic}"
)
story_chain = story_prompt | model | parser# Chain 2: Summarize whatever story it receives
summary_prompt = ChatPromptTemplate.from_template(
"Summarize this story in 5 words: {story}"
)
summary_chain = summary_prompt | model | parser# Combine: story_chain's output becomes summary_chain's input
full_chain = (
{"story": story_chain}
| summary_prompt
| model
| parser
)result = full_chain.invoke({"topic": "a robot learning to paint"})
print(result)
In plain English: the topic goes in → a story comes out → that story becomes the input for the summarizer → a 5-word summary comes out. Just like Station 1 passing the frame to Station 2 on the assembly line.
Example 4: RunnableParallel — running things at the same time
Sometimes you don’t want steps one after another — you want several things to happen simultaneously on the same input, then combine the results.
Analogy: instead of one assembly line, imagine three separate chefs cooking three different dishes at the same time from the same order — then a waiter brings all three dishes to the table together.
from langchain_core.runnables import RunnableParalleljoke_prompt = ChatPromptTemplate.from_template("Tell a joke about {topic}")
fact_prompt = ChatPromptTemplate.from_template("Tell one fact about {topic}")joke_chain = joke_prompt | model | parser
fact_chain = fact_prompt | model | parser# Run both chains in parallel on the same input
parallel_chain = RunnableParallel(joke=joke_chain, fact=fact_chain)result = parallel_chain.invoke({"topic": "cats"})
print(result)
# {'joke': '...', 'fact': '...'}
Both chains run at the same time (faster than doing them one by one) and hand back a dictionary with both results.
Example 4b: Parallel + a summary combiner
You can take this further — run things in parallel, then feed both results into one more step that ties them together:
combine_prompt = ChatPromptTemplate.from_template(
"Here's a joke: {joke}\nHere's a fact: {fact}\nWrite one witty sentence combining both."
)combined_chain = (
RunnableParallel(joke=joke_chain, fact=fact_chain)
| combine_prompt
| model
| parser
)result = combined_chain.invoke({"topic": "cats"})
print(result)
Now the joke and the fact both run at the same time, and their outputs get merged into one final witty line. Parallel steps and sequential steps, chained together — no special-case class needed for either.
Example 5: RunnableLambda — plugging in your own custom function
What if you want a plain Python function to be part of the chain — like converting text to uppercase, or some other custom logic? You wrap it in RunnableLambda, and now it behaves like every other Runnable — pluggable with |.
from langchain_core.runnables import RunnableLambdadef make_uppercase(text):
return text.upper()uppercase_step = RunnableLambda(make_uppercase)chain = prompt | model | parser | uppercase_stepresult = chain.invoke({"ingredient": "paneer"})
print(result) # the whole recipe, but SHOUTED IN CAPS
This is powerful: it means any Python logic can become a link in your chain, not just LangChain’s built-in pieces.
Example 5b: A word-count RunnableLambda
Here’s a second, super quick example — say you want to know how long the model’s answer is, without changing the answer itself:
def word_count(text):
print(f"(Response is {len(text.split())} words)")
return textcounter_step = RunnableLambda(word_count)chain = prompt | model | parser | counter_stepresult = chain.invoke({"ingredient": "tofu"})
word_count just peeks at the text, prints a little note, and passes it straight through unchanged. Handy for logging, debugging, or adding side effects mid-chain.
Example 6: RunnableBranch — conditional chains (if/else logic)
Sometimes you want the chain to behave differently depending on the input — like a fork in the road.
from langchain_core.runnables import RunnableBranchpositive_prompt = ChatPromptTemplate.from_template(
"Write an enthusiastic reply to: {text}"
)
negative_prompt = ChatPromptTemplate.from_template(
"Write an empathetic, calming reply to: {text}"
)def is_negative(input_dict):
return "bad" in input_dict["text"].lower()branch_chain = RunnableBranch(
(is_negative, negative_prompt | model | parser),
positive_prompt | model | parser, # default (fallback) branch
)result = branch_chain.invoke({"text": "I had a really bad day"})
print(result)
In plain English: “If the input text contains the word ‘bad’, use the calming reply chain. Otherwise, use the enthusiastic reply chain.” Just like a traffic light directing cars down different roads.
6. Putting It All Together — The Big Picture
Here’s the mental model to keep forever:
RUNNABLE
(anything that takes input → gives output)
|
┌─────────────┬───────────────┬────────────────┐
| | | |
Prompt Model Parser Custom Function
(Runnable) (Runnable) (Runnable) (Runnable)
| | | |
└──────────────┴───────────────┴────────────────┘
|
CONNECTED WITH "|"
|
= A CHAIN
(which is ALSO a Runnable itself!)That last line is the “aha” moment: a Chain is just a bigger Runnable made of smaller Runnables. That’s why you can take a whole chain and plug it into another chain, like we did in Example 3 (and Example 4b). It’s Runnables all the way down.
Quick Recap (Cheat Sheet)
Concept Plain English Meaning LangChain A toolbox for connecting AI models with prompts, data, and logic The old problem Every chain type was its own rigid class with different rules and methods — hard to mix and extend Runnable Anything with an input → output shape (prompt, model, parser, function, even a whole chain) — one universal shape that fixed the old problem Chain Multiple Runnables connected together, like an assembly line | (pipe) "Send this output to the next step" .invoke() Run once .batch() Run on multiple inputs at once .stream() Get the answer piece by piece RunnableParallel Run multiple chains at the same time on the same input RunnableLambda Plug your own Python function into a chain RunnableBranch Add if/else logic to a chain
And that’s the whole picture — from a confusing pipe symbol to a system that actually makes a lot of sense once you’ve seen the “before” and “after.” If you found this useful, go build your own 3-step chain today — even something simple like “translate → summarize → convert to bullet points.” Once you’ve built one with your own hands, that pipe symbol will never scare you again. Promise.
Happy building! 🚀







