LangChain Hands-on · Module 1 · Lab 04

Chains with LCEL & Runnables

⏱ 20 min + hands-on🎯 Beginner🖼 2 diagrams🧪 3 live demos🐍 Python code

A chain joins steps so the output of one becomes the input of the next. LCEL (LangChain Expression Language) writes it with the pipe |, exactly like Linux: prompt | llm | parser. In this lab you build sequential blog writers, a review analyser that runs three chains at once, a router that sends coding questions to a coding chain, and a fallback for when a model fails.

In this session

  • What a chain is and why the | pipe
  • prompt | llm | StrOutputParser()
  • Sequential chains: output of one → input of next
  • Passthrough, Parallel, Lambda and Branch
  • Fallbacks and retry for production
  • Stream and batch a whole chain for free

01The big idea: a factory assembly line

🏭

Think of a dosa counter. One person pours batter, the next adds masala, the last folds and serves. Each person does one job and passes the plate on. A chain is that line: prompt fills the template, the LLM writes, the parser cleans up. You can add, remove or swap one station without touching the others.

Demo · Watch data move through prompt | llm | parser

This is the real graph LangChain builds for that chain (cell chain.get_graph().print_ascii() from the notebook, run here):

+-------------+ | PromptInput | +-------------+ * * * +----------------+ | PromptTemplate | +----------------+ * * * +----------+ | ChatGroq | +----------+ * * * +-----------------+ | StrOutputParser | +-----------------+ * * * +-----------------------+ | StrOutputParserOutput | +-----------------------+

02The five runnables you will use every day

RunnableWhat it doesNotebook example
a | brun a, give its output to bprompt | llm | StrOutputParser()
{"title": chain1} | chain2a dict becomes RunnableParallel: build the next inputtitle chain → blog intro chain
RunnablePassthrough.assign(x=chain)keep the input and add a new keykeep topic, add title
RunnableParallel(a=…, b=…)run branches at the same time on the same inputsummary + sentiment + keywords of a hotel review
RunnableLambda(fn) or a plain functionany Python step inside a chaincount words, upper-case
RunnableBranch((cond, chain), default)if / else routingcoding question → coding chain
.with_fallbacks([b]), .with_retry()production safetybad model name → backup model
RunnableParallelsame review textsummary_chain“Clean room, polite staff, unreliable WiFi.”sentiment_chain“neutral”keywords_chain“clean room, polite staff, WiFi”
One input, three chains running at the same time: faster than calling them one by one.
Demo · Router: where does this question go?

03Hands-on: run it here

Every runnable from the notebook, with real outputs. The stand-in LLM answers by keyword, so even batch and parallel runs are repeatable.

lab04_lcel.py
from standins import RuleModel
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableParallel, RunnableLambda, RunnableBranch

llm = RuleModel(rules={
    "blog title": "Smart Farms: How AI Is Changing Indian Agriculture",
    "blog intro": "From Punjab's wheat fields to Kerala's spice gardens, AI is quietly helping farmers...",
    "summarise": "Clean room and polite staff, but unreliable WiFi.",
    "sentiment": "neutral",
    "keywords": "clean room, polite staff, WiFi",
    "'coding' or 'general'": "coding",
    "python code": "text[::-1]  # slicing reverses a string",
    "2 lines": "Sunlight scatters off air molecules. Blue light scatters most, so the sky looks blue.",
    "tweet": "Monsoon is here! Chai, pakoras and the smell of first rain. #Monsoon",
}, default="OK")

chain = PromptTemplate.from_template("Write a tweet about {topic}") | llm | StrOutputParser()
print("1 chain     :", chain.invoke({"topic": "monsoon"}))
print("2 + lambda  :", (chain | RunnableLambda(lambda t: {"words": len(t.split())})).invoke({"topic": "monsoon"}))

title_chain = PromptTemplate.from_template("Give one catchy blog title about {topic}. Only the title.") | llm | StrOutputParser()
blog_chain = PromptTemplate.from_template("Write a 100-word blog intro for the title: {title}") | llm | StrOutputParser()
print("3 sequential:", ({"title": title_chain} | blog_chain).invoke({"topic": "AI in Indian agriculture"}))
print("4 assign    :", RunnablePassthrough.assign(title=title_chain).invoke({"topic": "AI in Indian agriculture"}))

p = lambda t: PromptTemplate.from_template(t) | llm | StrOutputParser()
analyzer = RunnableParallel(summary=p("Summarise in 1 line: {text}"), sentiment=p("Sentiment (positive/negative/neutral): {text}"),
                            keywords=p("Give 3 keywords, comma separated: {text}"))
print("5 parallel  :", analyzer.invoke({"text": "The hotel room was clean and staff were very polite, but the WiFi kept disconnecting."}))

class_llm = RuleModel(rules={"reverse": "coding"}, default="general")
classify = PromptTemplate.from_template("Is this question about 'coding' or 'general'? One word.\nQuestion: {question}") | class_llm | StrOutputParser()
router = RunnablePassthrough.assign(kind=classify) | RunnableBranch(
    (lambda x: "coding" in x["kind"].lower(), p("Answer with a short Python code example: {question}")),
    p("Answer simply in 2 lines: {question}"))
for q in ["How to reverse a string in Python?", "Why is the sky blue?"]:
    print("6 router    :", q, "->", router.invoke({"question": q}))

class Broken(RuleModel):
    def _generate(self, *a, **k): raise ConnectionError("model-that-does-not-exist")
safe = Broken().with_fallbacks([RuleModel(default="fallback worked")])
print("7 fallback  :", safe.invoke("Say: fallback worked").content)
1 chain : Monsoon is here! Chai, pakoras and the smell of first rain. #Monsoon 2 + lambda : {'words': 12} 3 sequential: From Punjab's wheat fields to Kerala's spice gardens, AI is quietly helping farmers... 4 assign : {'topic': 'AI in Indian agriculture', 'title': 'Smart Farms: How AI Is Changing Indian Agriculture'} 5 parallel : {'summary': 'Clean room and polite staff, but unreliable WiFi.', 'sentiment': 'neutral', 'keywords': 'clean room, polite staff, WiFi'} 6 router : How to reverse a string in Python? -> text[::-1] # slicing reverses a string 6 router : Why is the sky blue? -> Sunlight scatters off air molecules. Blue light scatters most, so the sky looks blue. 7 fallback : fallback worked
Runs here without any API key. A small stand-in plays the LLM (RuleModel or ScriptedModel from standins.py), so every output below is from a real run of this code. In Colab you swap one line: llm = ChatGroq(model="openai/gpt-oss-20b").

04Hands-on: Ajeetkumar’s Colab notebook

Colab setup (same in every notebook): click 🔑 Secrets in the Colab sidebar, add groq_key with your free key from console.groq.com, switch on Notebook access, then run the cells from the top.
Demo · Notebook player · Lab 04

05Benefits and the one limit

LCEL gives you• readable pipelines with |• stream, batch and async for free• parallel steps run at the same time• every step visible in LangSmithLimit• a chain is a fixed path• it cannot decide “which tool next?”• for loops and decisions → Agent (Lab 13)• or a LangGraph workflow (Lab 17)
Start with a chain. Move to an agent only when the steps are not fixed.

06Practice tasks

  1. Chain: topic → 5 interview questions → answers for each (sequential).
  2. Parallel chain: from a product name create a tagline, an Instagram caption and a tweet.
  3. Router: send Hindi questions to a Hindi chain, English to an English chain.

Try these in the same Colab notebook after you finish the lab. Change one thing at a time and look at the output again.

07Summary

08Quick quiz

1. What does | do in LCEL?

2. Which keeps the input AND adds a new key?

3. Summary, sentiment and keywords of the same text, fastest way?

4. Primary model name is wrong. What saves you?

5. When is a chain NOT enough?

09References

Created by Ajeetkumar