LangChain LCEL and Runnables
LangChain LCEL (LangChain Expression Language) is the way to connect components into a chain using the | pipe operator. Every prompt, model, parser and retriever is a Runnable, and Runnables can be piped together so the output of one becomes the input of the next.
Your First LCEL Chain
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_ollama import ChatOllama
prompt = ChatPromptTemplate.from_template("Write a haiku about {subject}.")
llm = ChatOllama(model="llama3.2", temperature=0.7)
chain = prompt | llm | StrOutputParser()
print(chain.invoke({"subject": "monsoon rain"}))Sample output: Grey clouds gather low Rooftops drum a steady song Green fields drink their fill
The dictionary goes into the prompt, the prompt value goes into the model, and the AIMessage goes into StrOutputParser, which returns just the text. The chain itself is a Runnable, so it also has batch, stream and the async methods.
The Runnable Interface
- invoke / ainvoke: one input, one output
- batch / abatch: many inputs in parallel
- stream / astream: stream output chunks
- with_retry(), with_fallbacks([...]): add resilience
- with_config(run_name=..., tags=[...]): label runs for tracing
RunnableParallel: Run Steps Side by Side
A dictionary of runnables runs each branch on the same input and collects the results:
from langchain_core.runnables import RunnableParallel
joke = ChatPromptTemplate.from_template("Tell a short joke about {topic}.") | llm | StrOutputParser()
fact = ChatPromptTemplate.from_template("Give one fun fact about {topic}.") | llm | StrOutputParser()
both = RunnableParallel(joke=joke, fact=fact)
result = both.invoke({"topic": "cats"})
print(result["fact"])Sample output: Cats spend about 70% of their lives sleeping.
RunnablePassthrough and RunnableLambda
RunnablePassthrough forwards its input unchanged, which is useful for keeping the original question while adding new keys. RunnableLambda turns any Python function into a Runnable. Plain functions inside a chain are wrapped automatically.
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
def shout(text: str) -> str:
return text.upper()
chain = (
{"subject": RunnablePassthrough()}
| prompt | llm | StrOutputParser()
| RunnableLambda(shout)
)
print(chain.invoke("coffee"))Sample output: DARK BREW IN THE CUP STEAM CURLS INTO MORNING LIGHT EYES SLOWLY AWAKEN
Here the chain accepts a plain string. The leading dictionary builds {"subject": "coffee"} for the prompt. This pattern appears again in RAG chains, where one branch fetches documents and another passes the question through.
Fallbacks
with_fallbacks lets a chain try a backup model if the first one fails, for example a local model with a hosted model as backup: llm.with_fallbacks([backup_llm]).
Frequently asked questions
Is LCEL still used in LangChain 1.0?
Yes. LCEL and Runnables live in langchain-core and are fully supported. For agents with loops and state, LangChain 1.0 recommends create_agent or LangGraph instead of long LCEL chains.
What does the pipe operator do in LangChain?
It composes two Runnables into a RunnableSequence, so the output of the left side becomes the input of the right side.
When should I use LangGraph instead of LCEL?
Use LCEL for straight pipelines. Use LangGraph when you need loops, branching based on model decisions, persistence or human approval steps.