Most LangChain tutorials drown you in abstractions before you can ship a single chain. This langchain expression language beginner guide cuts through that: LCEL is the declarative pipe operator (|) that composes prompts, models, and parsers into runnable sequences with built-in streaming and parallelism. You’ll build a working chain in the next ten minutes and learn where the footguns are.
Install the minimal surface area
LangChain’s package split means you no longer need the monolithic langchain meta-package for basic chains. Install only what you execute:
pip install langchain-core langchain-openai
langchain-core provides the LCEL primitives (Runnable, PromptTemplate, OutputParser). langchain-openai gives you the ChatOpenAI model wrapper. Avoid pulling langchain-community unless you need a specific integration; it drags in heavy optional deps.
Define a prompt and a model
Start by constructing a ChatPromptTemplate. LCEL treats prompts as runnables that accept a dict and return a list of messages.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You are a terse senior engineer. Answer in one sentence."),
("user", "Explain {concept} to a junior dev.")
])
The model is also a runnable. Point it at any OpenAI-compatible endpoint. If you’d rather not manage multiple provider keys, point ChatOpenAI at an OpenAI-compatible gateway such as n4n.ai; its single endpoint fronts 240+ models and auto-falls back when a provider is rate-limited, so your LCEL chain stays unchanged.
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
# base_url="https://api.n4n.ai/v1", # optional override
# api_key="your-key",
)
Pitfall: model expects message inputs, not raw strings. The prompt runnable handles that conversion. Don’t call model.invoke("text") directly unless you’ve wrapped it.
Compose your first chain with the pipe operator
LCEL’s core idiom is runnable_a | runnable_b. The output type of the left side must match the input type of the right side.
chain = prompt | model
response = chain.invoke({"concept": "vector embeddings"})
print(response.content)
That’s a complete chain. Under the hood, prompt returns list[BaseMessage], model accepts that and returns an AIMessage. No subclassing, no Chain boilerplate.
If you need to inspect intermediate values, use RunnablePassthrough to echo inputs:
from langchain_core.runnables import RunnablePassthrough
debug_chain = {"prompt_input": RunnablePassthrough()} | prompt | model
Add output parsing and fallbacks
Raw model output is an AIMessage. For most apps you want a string or structured object. Append a parser:
from langchain_core.output_parsers import StrOutputParser
chain = prompt | model | StrOutputParser()
text = chain.invoke({"concept": "rate limiting"})
For structured output, use PydanticOutputParser or JsonOutputParser. Bind the schema to the model when possible to get native function-calling:
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
class Definition(BaseModel):
term: str = Field(description="the concept name")
summary: str = Field(description="one-line explanation")
parser = JsonOutputParser(pydantic_object=Definition)
structured_chain = prompt | model.with_structured_output(Definition)
Fallbacks are first-class. If gpt-4o-mini is degraded, LCEL retries on a secondary runnable:
backup_model = ChatOpenAI(model="gpt-3.5-turbo")
chain_with_fallback = (prompt | model | StrOutputParser()).with_fallbacks(
[prompt | backup_model | StrOutputParser()]
)
Tradeoff: fallback chains duplicate the prompt execution. For expensive prompts, consider RunnableParallel with a primary/secondary race only if latency budgets allow.
Streaming and async execution
LCEL chains are streaming-native. Append a parser that supports streaming (like StrOutputParser) and call .stream():
for chunk in chain.stream({"concept": "context windows"}):
print(chunk, end="", flush=True)
The tokens arrive as they are generated; the parser reconstructs partial strings. For concurrent workloads, use async:
async def main():
result = await chain.ainvoke({"concept": "token billing"})
return result
# asyncio.run(main())
Pitfall: mixing sync invoke inside an async event loop blocks. Use ainvoke or run sync chains in a thread executor.
Common pitfalls and tradeoffs
Variable name mismatches. The dict keys you pass to invoke must match the {placeholders} in the prompt. A missing key raises KeyError at runtime, not import time. Validate prompts with prompt.input_variables.
Hidden latency from sequential runs. If you need the model output and an independent API call, don’t pipe them. Use RunnableParallel:
from langchain_core.runnables import RunnableParallel
parallel = RunnableParallel({
"llm": chain,
"static": lambda x: "cached metadata"
})
This runs both branches concurrently. Sequential piping forces a wait.
Over-composing. LCEL is not a substitute for application logic. Putting retry, branching, and DB calls all in one | expression makes debugging painful. Use RunnableBranch for conditionals and keep side effects in small custom runnables:
from langchain_core.runnables import RunnableLambda
def log_input(x):
print("invoking with", x)
return x
chain = RunnableLambda(log_input) | prompt | model
Graph introspection. When a chain misbehaves, call chain.get_graph().print_ascii() to see the actual topology. Beginners often assume the pipe order is linear; parallel branches and passthroughs change that.
Where to go next
This langchain expression language beginner guide covered the runnable mental model, composition, parsing, fallback, and streaming. The next steps are practical:
- Deploy the chain with
LangServeto get a FastAPI endpoint and a Playground UI for free. - Use
RunnableBatchto process lists with automatic concurrency limits. - Read the LCEL docs on
configurable_fieldsto swap models per request without rebuilding the chain.
LCEL rewards keeping chains small and typed. Write a runnable that does one thing, test it with .invoke on a fixture, then compose. That discipline beats a 200-line Agent class every time.