A "chain" is just steps connected end to end: prompt → model → parser. LangChain lets you build one with a single operator — the pipe | — borrowed from the Unix shell. This is LCEL: LangChain Expression Language, and it's the heart of the framework.
Your first chain
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = init_chat_model("claude-opus-5", model_provider="anthropic")
prompt = ChatPromptTemplate.from_messages([
("system", "You explain {subject} concepts to beginners."),
("user", "{question}"),
])
# read it left to right: fill prompt -> call model -> parse to string
chain = prompt | model | StrOutputParser()
print(chain.invoke({"subject": "DBMS", "question": "What is normalization?"}))That prompt | model | parser reads like a sentence. Data flows left to right; the output of each step becomes the input of the next. The whole chain is itself a Runnable — so it has .invoke() too, and can be dropped inside a bigger chain.
Why the pipe matters
Because every piece speaks the same interface, you get three things for free on any chain:
- .invoke(x) — run once, get the answer.
- .batch([x, y, z]) — run many inputs efficiently.
- .stream(x) — get tokens as they're generated (for a typing effect).
# streaming — print the answer word-by-word as it arrives
for piece in chain.stream({"subject": "networks", "question": "What is DNS?"}):
print(piece, end="", flush=True)Running steps in parallel
Need two things from one input at once? A dict of Runnables runs them in parallel and merges the results.
from langchain_core.runnables import RunnableParallel
summary = ChatPromptTemplate.from_template("Summarize in 1 line: {text}") | model | StrOutputParser()
keywords = ChatPromptTemplate.from_template("List 3 keywords for: {text}") | model | StrOutputParser()
both = RunnableParallel(summary=summary, keywords=keywords)
result = both.invoke({"text": "LangChain is a framework for building LLM apps."})
print(result["summary"])
print(result["keywords"])Passing data through
RunnablePassthrough forwards the input untouched — essential for RAG, where you need to keep the original question and add retrieved context. We use it heavily in the RAG post.
from langchain_core.runnables import RunnablePassthrough
chain = {"question": RunnablePassthrough()} | prompt2 | model | StrOutputParser()The takeaway
LCEL turns "call the model, then do the next thing, then the next" into one readable expression that streams, batches, and parallelizes automatically. Next we give a chain a memory so it remembers the conversation. Related: core concepts.
← All Articles