Go Deeper 6 min read Updated Sep 11, 2026

LCEL: Composing Chains with the Pipe Operator

The prompts, models, and output parsers guide showed you prompt | model | parser without dwelling on what the | is actually doing. It's not just syntax sugar, it's LCEL, the LangChain Expression Language, and understanding it properly is what lets you build more than a three-step chain without fighting the framework.

Everything is a Runnable

A prompt template, a chat model, an output parser, and a chain made of all three are all the same underlying type: a Runnable. Every Runnable implements the same small interface, invoke, stream, batch, and their async equivalents, which is exactly what makes the | operator work. Piping two Runnables together just wires one's output to be the next one's input, and the result is itself a new Runnable you can pipe further, invoke, or stream.

chain = prompt | model | parser
chain.invoke({"topic": "otters"})   # runs all three steps
chain.stream({"topic": "otters"})   # streams the model's tokens through
chain.batch([{"topic": "otters"}, {"topic": "cats"}])  # runs both concurrently

That's the actual payoff of LCEL: you write the chain once, and streaming, batching, and async all come for free, because every piece already knows how to do all three. You don't implement streaming for your specific chain, you get it because every Runnable in it already supports it.

Running steps in parallel with RunnableParallel

Not everything is a straight line. If you need to run two independent steps and combine their results, like generating a summary and extracting keywords from the same input, RunnableParallel runs them concurrently instead of one after another:

from langchain_core.runnables import RunnableParallel

chain = RunnableParallel(
    summary=summary_chain,
    keywords=keyword_chain,
)
result = chain.invoke({"text": long_article})
# result = {"summary": "...", "keywords": [...]}

Dropping in custom logic with RunnableLambda

Sometimes a step in your chain isn't a prompt or a model call, it's just a plain function: reformatting a string, filtering a list, calling an internal API. RunnableLambda wraps any Python function so it can sit in the pipe chain like anything else:

from langchain_core.runnables import RunnableLambda

uppercase = RunnableLambda(lambda x: x.upper())
chain = prompt | model | parser | uppercase
In practice: you rarely reach for RunnableLambda or RunnableParallel by name until you need them, most chains really are just a straight prompt | model | parser pipe. Knowing they exist matters the day your chain needs a branch or a custom step and you'd otherwise be tempted to break out of LCEL entirely to handle it.

Where LCEL stops being the right tool

LCEL is built for chains that are linear or branch a fixed, known number of ways, decided when you write the code. The moment you need a loop (retry until a condition passes, keep calling tools until the model says it's done) or a decision about which branch to take that depends on runtime state, you've left LCEL's actual design target. That's precisely the gap LangGraph was built to fill, and it's worth reading as "a different tool for a different shape of problem" rather than "the more advanced version of LCEL."

Share this guide

Was this guide helpful?

Thanks for the feedback!

Want more hands-on AI builds like this?

APA Mastery runs live, practical sessions on working with modern AI tools - not just theory.

See What's On →
← PreviousMemory in LangChain