LCEL โ€” The Expression Language

Lesson 3: LCEL โ€” The Expression Language

LCEL (LangChain Expression Language) is the composition layer that turns LangChain components into pipelines. It's the difference between writing a pipeline as nested function calls and writing it as a readable chain: prompt | model | parser. The pipe operator is not just syntax sugar โ€” every piece of an LCEL chain implements a common Runnable interface, which buys you a lot for free.

Key idea: LCEL chains are runnables. Anything you can build with | inherits the same superpowers: invoke(), stream(), batch(), and their async versions โ€” without writing any glue code.

The Runnable interface

Every component (model, prompt, parser, retriever, tool, or composed chain) exposes the same methods:

MethodWhat it does
invoke(input)Run once, return the result
stream(input)Run, yielding output chunks as they arrive (token-by-token for models)
batch(inputs)Run over a list of inputs, returning a list of results
ainvoke / astream / abatchAsync equivalents

Because streaming, batching, and async come from the interface rather than each component, a complex chain streams just like a bare model call. That's the "free" part.

Composition primitives

from langchain_core.runnables import RunnablePassthrough, RunnableLambda

# Pipe: feed previous output into next
chain = prompt | model | StrOutputParser()

# RunnablePassthrough: pass input through unchanged (or assign new keys)
passthrough = RunnablePassthrough.assign(upcased=lambda x: x["topic"].upper())

# RunnableLambda: wrap arbitrary Python as a chain step
def word_count(text: str) -> int:
    return len(text.split())
step = RunnableLambda(word_count)

# Parallel: run multiple chains on the same input, merge results
parallel = {
    "summary": summary_chain,
    "keywords": keyword_chain,
} | RunnableLambda(merge_results)
LCEL PIPELINE: prompt | model | parser prompt | model | parser Result: string output, streamed token-by-token, batchable, async-ready

Resilience features

LCEL runnables include built-in failure handling that would otherwise be a lot of boilerplate:

  • Retries โ€” chain.with_retry(stop_after_attempt=3) handles transient API errors.
  • Fallbacks โ€” model.with_fallbacks([cheaper_model, local_model]) degrades gracefully when a provider is down or rate-limited.
  • Configuration โ€” .with_config({"max_concurrency": 5}) for batch runs; .bind(...) to pin model parameters.
  • Timeouts โ€” .with_timeout(30) for hard deadlines.
Pitfall: LCEL is powerful but linear. It has no native concept of state that persists across runs or loops back to an earlier step. Teams that try to bolt loops onto LCEL usually end up rebuilding a graph engine badly โ€” which is exactly why LangGraph exists.
Rule of thumb: LCEL for linear/parallel pipelines with streaming and retries. When control flow needs to branch based on model output or remember history across calls, move the orchestration to LangGraph.

๐Ÿง  Knowledge Check

1. What does the | operator do in LCEL?

2. Which superpowers does every LCEL runnable get for free?

3. What is LCEL NOT well-suited for?

Further Reading