What Are Chains?

Chains are sequences of steps that process data. Think of them like an assembly line in a factory - each station does one job and passes the result to the next.

# A Simple Chain: Like an Assembly Line

User Input: "Explain quantum computing"
                    │
                    ▼
┌─────────────────────────────────────────────────────────────────┐
│  STEP 1: Format Prompt                                          │
│  "You are a teacher. Explain {topic} simply."                   │
│  Result: "You are a teacher. Explain quantum computing simply." │
└─────────────────────────────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────────────┐
│  STEP 2: Send to LLM                                            │
│  GPT-4 processes the prompt                                     │
│  Result: "Quantum computing uses quantum bits (qubits)..."      │
└─────────────────────────────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────────────┐
│  STEP 3: Parse Output                                           │
│  Extract just the text                                          │
│  Result: Clean string output                                    │
└─────────────────────────────────────────────────────────────────┘
                    │
                    ▼
Final Output: "Quantum computing uses quantum bits (qubits)..."

Why Use Chains?

  • Modularity: Each step does one thing well
  • Reusability: Use the same steps in different chains
  • Readability: Easy to understand the data flow
  • Debugging: Test each step independently
  • Flexibility: Swap components easily

What is LCEL?

LCEL (LangChain Expression Language) is the modern way to build chains in LangChain. It uses the | (pipe) operator to connect components, making chains look clean and readable.

# LCEL: The Pipe Operator

# Think of | like a conveyor belt moving data from left to right

prompt | llm | output_parser

# This means:
# 1. Take input
# 2. Pass through prompt template
# 3. Pass result to LLM
# 4. Pass LLM output to parser
# 5. Return parsed result

Old Way vs LCEL (New Way)

# ❌ OLD WAY: Verbose and Nested
from langchain.chains import LLMChain

chain = LLMChain(
    llm=llm,
    prompt=prompt,
    output_parser=parser
)
result = chain.run(topic="AI")


# ✅ NEW WAY (LCEL): Clean and Readable
chain = prompt | llm | parser
result = chain.invoke({"topic": "AI"})

LCEL Benefits:

  • Cleaner, more readable syntax
  • Built-in streaming support
  • Automatic async support
  • Built-in retry and fallback handling
  • Easy parallel execution

Your First LCEL Chain

Let's build a simple chain step by step:

from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

# STEP 1: Create the components

# The LLM (the brain)
llm = ChatOpenAI(model="gpt-4", temperature=0.7)

# The prompt template (the instructions)
prompt = ChatPromptTemplate.from_template(
    "You are a helpful assistant. Explain {topic} in simple terms for a beginner."
)

# The output parser (cleans up the response)
output_parser = StrOutputParser()


# STEP 2: Connect them with the pipe operator

chain = prompt | llm | output_parser
#       │         │         │
#       │         │         └── 3. Extract text from response
#       │         └──────────── 2. Send to GPT-4
#       └────────────────────── 1. Format the prompt


# STEP 3: Run the chain

result = chain.invoke({"topic": "machine learning"})
print(result)

# Output: "Machine learning is like teaching a computer to learn
#          from examples, just like how you learned to recognize..."

Understanding LCEL Components

LCEL works with "Runnables" - any component that can process input and produce output.

# Common LCEL Components

┌─────────────────────────────────────────────────────────────────┐
│ COMPONENT              │ WHAT IT DOES                          │
├─────────────────────────────────────────────────────────────────┤
│ ChatPromptTemplate     │ Formats user input into a prompt      │
│ ChatOpenAI / LLM       │ Processes prompt, generates response  │
│ StrOutputParser        │ Extracts string from LLM response     │
│ JsonOutputParser       │ Parses JSON from LLM response         │
│ RunnableLambda         │ Runs any Python function              │
│ RunnablePassthrough    │ Passes input unchanged                │
│ RunnableParallel       │ Runs multiple chains simultaneously   │
│ RunnableBranch         │ Routes to different chains            │
└─────────────────────────────────────────────────────────────────┘

The Invoke Methods

# Different ways to run a chain

chain = prompt | llm | output_parser

# 1. invoke() - Single input, wait for complete response
result = chain.invoke({"topic": "AI"})

# 2. stream() - Single input, get response as it generates
for chunk in chain.stream({"topic": "AI"}):
    print(chunk, end="", flush=True)

# 3. batch() - Multiple inputs at once
results = chain.batch([
    {"topic": "AI"},
    {"topic": "blockchain"},
    {"topic": "quantum computing"}
])

# 4. ainvoke() - Async version of invoke
result = await chain.ainvoke({"topic": "AI"})

# 5. astream() - Async streaming
async for chunk in chain.astream({"topic": "AI"}):
    print(chunk, end="", flush=True)

Building More Complex Chains

Chain with Multiple Variables

from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-4")

# Prompt with multiple placeholders
prompt = ChatPromptTemplate.from_template("""
You are an expert {role}.
Write a {length} explanation about {topic}.
Target audience: {audience}
""")

chain = prompt | llm | StrOutputParser()

# All variables are passed in the invoke dictionary
result = chain.invoke({
    "role": "science teacher",
    "length": "brief",
    "topic": "photosynthesis",
    "audience": "5th grade students"
})

print(result)

Chain with System and Human Messages

from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain_core.messages import SystemMessage

# More control over message types
prompt = ChatPromptTemplate.from_messages([
    SystemMessage(content="You are a professional translator. Translate accurately."),
    HumanMessagePromptTemplate.from_template(
        "Translate this from {source_lang} to {target_lang}: {text}"
    )
])

chain = prompt | llm | StrOutputParser()

result = chain.invoke({
    "source_lang": "English",
    "target_lang": "Spanish",
    "text": "Hello, how are you today?"
})

print(result)  # "Hola, ¿cómo estás hoy?"

RunnableLambda: Custom Functions in Chains

Use RunnableLambda to insert any Python function into your chain:

from langchain_core.runnables import RunnableLambda

# Custom function to process data
def format_output(text: str) -> str:
    """Add formatting to the output."""
    return f"📝 Summary:\n{'-' * 40}\n{text}\n{'-' * 40}"

def word_count(text: str) -> dict:
    """Count words and return with original text."""
    return {
        "text": text,
        "word_count": len(text.split())
    }

# Create runnables from functions
formatter = RunnableLambda(format_output)
counter = RunnableLambda(word_count)

# Build chain with custom functions
chain = prompt | llm | StrOutputParser() | formatter

result = chain.invoke({"topic": "Python programming"})
print(result)

# Output:
# 📝 Summary:
# ----------------------------------------
# Python is a versatile programming language...
# ----------------------------------------


# Or chain multiple custom functions
chain_with_count = prompt | llm | StrOutputParser() | counter

result = chain_with_count.invoke({"topic": "AI"})
print(f"Response: {result['text'][:100]}...")
print(f"Word count: {result['word_count']}")

Shorthand: Using Lambda Directly

# You can use regular lambdas for simple operations
chain = (
    prompt
    | llm
    | StrOutputParser()
    | (lambda x: x.upper())  # Convert to uppercase
    | (lambda x: f"RESULT: {x}")  # Add prefix
)

result = chain.invoke({"topic": "hello world"})
# Output: "RESULT: HELLO WORLD IS A COMMON..."

RunnablePassthrough: Preserving Input

Sometimes you need to pass the original input alongside processed data:

from langchain_core.runnables import RunnablePassthrough, RunnableParallel

# Problem: After LLM processes, we lose the original question
# Solution: Use RunnablePassthrough to keep it

prompt = ChatPromptTemplate.from_template(
    "Answer this question: {question}"
)

# RunnableParallel runs multiple things and combines results
chain = RunnableParallel(
    # Pass through the original question unchanged
    original_question=RunnablePassthrough(),
    # Also get the LLM's answer
    answer=prompt | llm | StrOutputParser()
)

result = chain.invoke({"question": "What is Python?"})

print(f"Question: {result['original_question']}")
print(f"Answer: {result['answer']}")

# Output:
# Question: {'question': 'What is Python?'}
# Answer: Python is a high-level programming language...

Extracting Specific Fields

from operator import itemgetter

# Use itemgetter to extract specific fields
chain = RunnableParallel(
    question=itemgetter("question"),  # Extract just the question string
    context=itemgetter("context"),    # Extract context
    answer=prompt | llm | StrOutputParser()
)

result = chain.invoke({
    "question": "What is the capital?",
    "context": "France is a country in Europe."
})

print(result)
# {
#   'question': 'What is the capital?',
#   'context': 'France is a country in Europe.',
#   'answer': 'The capital of France is Paris.'
# }

RunnableParallel: Running Things Simultaneously

Execute multiple operations at the same time for better performance:

from langchain_core.runnables import RunnableParallel

# Create different prompts for different purposes
summary_prompt = ChatPromptTemplate.from_template(
    "Summarize this in 2 sentences: {text}"
)

keywords_prompt = ChatPromptTemplate.from_template(
    "Extract 5 keywords from this text: {text}"
)

sentiment_prompt = ChatPromptTemplate.from_template(
    "What is the sentiment of this text (positive/negative/neutral)? {text}"
)

# Run all three in parallel!
analysis_chain = RunnableParallel(
    summary=summary_prompt | llm | StrOutputParser(),
    keywords=keywords_prompt | llm | StrOutputParser(),
    sentiment=sentiment_prompt | llm | StrOutputParser()
)

# All three LLM calls happen simultaneously
result = analysis_chain.invoke({
    "text": """
    The new iPhone is absolutely amazing! The camera quality is exceptional,
    and the battery life has improved significantly. Best purchase I've made
    this year. Highly recommend to everyone looking for a premium smartphone.
    """
})

print("Summary:", result["summary"])
print("Keywords:", result["keywords"])
print("Sentiment:", result["sentiment"])

# This is 3x faster than running them one after another!
# Visual representation of parallel execution

                         Input Text
                              │
              ┌───────────────┼───────────────┐
              │               │               │
              ▼               ▼               ▼
        ┌──────────┐   ┌──────────┐   ┌──────────┐
        │ Summary  │   │ Keywords │   │Sentiment │
        │  Prompt  │   │  Prompt  │   │  Prompt  │
        └────┬─────┘   └────┬─────┘   └────┬─────┘
              │               │               │
              ▼               ▼               ▼
        ┌──────────┐   ┌──────────┐   ┌──────────┐
        │   LLM    │   │   LLM    │   │   LLM    │
        │  Call 1  │   │  Call 2  │   │  Call 3  │
        └────┬─────┘   └────┬─────┘   └────┬─────┘
              │               │               │
              └───────────────┼───────────────┘
                              │
                              ▼
                    Combined Results
                    {
                      "summary": "...",
                      "keywords": "...",
                      "sentiment": "..."
                    }

RunnableBranch: Conditional Routing

Route to different chains based on conditions:

from langchain_core.runnables import RunnableBranch

# Different prompts for different question types
code_prompt = ChatPromptTemplate.from_template(
    "You are a coding expert. Help with: {question}"
)

math_prompt = ChatPromptTemplate.from_template(
    "You are a math tutor. Solve: {question}"
)

general_prompt = ChatPromptTemplate.from_template(
    "You are a helpful assistant. Answer: {question}"
)

# Classification function
def classify_question(input_dict):
    question = input_dict["question"].lower()
    if any(word in question for word in ["code", "python", "javascript", "programming", "function"]):
        return "code"
    elif any(word in question for word in ["calculate", "math", "equation", "solve", "number"]):
        return "math"
    return "general"

# Create the routing chain
router = RunnableBranch(
    # (condition, chain) pairs
    (lambda x: classify_question(x) == "code", code_prompt | llm | StrOutputParser()),
    (lambda x: classify_question(x) == "math", math_prompt | llm | StrOutputParser()),
    # Default chain (no condition)
    general_prompt | llm | StrOutputParser()
)

# Test with different questions
print(router.invoke({"question": "Write a Python function to sort a list"}))
# Routes to code_prompt

print(router.invoke({"question": "Calculate 15% of 200"}))
# Routes to math_prompt

print(router.invoke({"question": "What is the capital of France?"}))
# Routes to general_prompt (default)
# Visual representation of branching

                         Input Question
                              │
                              ▼
                    ┌─────────────────┐
                    │   CLASSIFIER    │
                    │  "What type?"   │
                    └─────────────────┘
                              │
         ┌────────────────────┼────────────────────┐
         │                    │                    │
    "code"               "math"             "general"
         │                    │                    │
         ▼                    ▼                    ▼
   ┌───────────┐       ┌───────────┐       ┌───────────┐
   │   Code    │       │   Math    │       │  General  │
   │   Chain   │       │   Chain   │       │   Chain   │
   └───────────┘       └───────────┘       └───────────┘
         │                    │                    │
         └────────────────────┼────────────────────┘
                              │
                              ▼
                         Response

Chaining Multiple LLM Calls

Sometimes you need one LLM call to feed into another:

from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

# Chain 1: Generate a story outline
outline_prompt = ChatPromptTemplate.from_template(
    "Create a brief 3-point outline for a story about: {topic}"
)

# Chain 2: Write the story based on the outline
story_prompt = ChatPromptTemplate.from_template(
    """Based on this outline:
{outline}

Write a short story (about 200 words)."""
)

# Chain 3: Create a title for the story
title_prompt = ChatPromptTemplate.from_template(
    """Based on this story:
{story}

Create a catchy title."""
)

# Connect them: Topic → Outline → Story → Title
full_chain = (
    # Step 1: Generate outline
    {"outline": outline_prompt | llm | StrOutputParser()}
    # Step 2: Pass outline to story prompt
    | {"story": story_prompt | llm | StrOutputParser(), "outline": itemgetter("outline")}
    # Step 3: Generate title from story
    | {"title": title_prompt | llm | StrOutputParser(), "story": itemgetter("story"), "outline": itemgetter("outline")}
)

result = full_chain.invoke({"topic": "a robot learning to paint"})

print("=== OUTLINE ===")
print(result["outline"])
print("\n=== STORY ===")
print(result["story"])
print("\n=== TITLE ===")
print(result["title"])

Simpler Sequential Chain

# For simpler cases, chain directly

first_prompt = ChatPromptTemplate.from_template(
    "Translate to French: {text}"
)

second_prompt = ChatPromptTemplate.from_template(
    "Now make this text more formal: {text}"
)

# Sequential: Translate, then formalize
chain = (
    first_prompt
    | llm
    | StrOutputParser()
    | {"text": RunnablePassthrough()}  # Wrap output for next prompt
    | second_prompt
    | llm
    | StrOutputParser()
)

result = chain.invoke({"text": "Hey, what's up?"})
print(result)  # Formal French translation

Streaming with LCEL

LCEL has built-in streaming support - show results as they generate:

# Streaming is automatic with LCEL!

chain = prompt | llm | StrOutputParser()

# Stream the response
print("Streaming response:")
for chunk in chain.stream({"topic": "the history of computers"}):
    print(chunk, end="", flush=True)

print("\n\nDone!")

# Output appears word by word as it generates:
# "The history of computers..." (appears gradually)

Async Streaming

import asyncio

async def stream_response(topic: str):
    chain = prompt | llm | StrOutputParser()

    async for chunk in chain.astream({"topic": topic}):
        print(chunk, end="", flush=True)

    print("\n")

# Run async
asyncio.run(stream_response("artificial intelligence"))

Streaming Events (Advanced)

# Get detailed events during streaming

async def stream_with_events(topic: str):
    chain = prompt | llm | StrOutputParser()

    async for event in chain.astream_events(
        {"topic": topic},
        version="v2"
    ):
        kind = event["event"]

        if kind == "on_chat_model_stream":
            # Token being generated
            content = event["data"]["chunk"].content
            print(content, end="", flush=True)

        elif kind == "on_chain_start":
            print(f"\n[Chain started: {event['name']}]")

        elif kind == "on_chain_end":
            print(f"\n[Chain ended: {event['name']}]")

asyncio.run(stream_with_events("quantum physics"))

Error Handling and Fallbacks

LCEL provides elegant error handling:

from langchain_openai import ChatOpenAI

# Primary model (might be expensive or rate-limited)
primary_llm = ChatOpenAI(model="gpt-4", temperature=0)

# Fallback model (cheaper, more available)
fallback_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)

# Create chain with fallback
chain_with_fallback = (
    prompt
    | primary_llm.with_fallbacks([fallback_llm])
    | StrOutputParser()
)

# If GPT-4 fails (rate limit, error), automatically tries GPT-3.5
result = chain_with_fallback.invoke({"topic": "AI safety"})

Retry Configuration

# Add retry logic
chain_with_retry = (
    prompt
    | llm.with_retry(
        stop_after_attempt=3,  # Try up to 3 times
        wait_exponential_jitter=True  # Wait longer between retries
    )
    | StrOutputParser()
)

result = chain_with_retry.invoke({"topic": "resilient systems"})

Custom Error Handling

from langchain_core.runnables import RunnableLambda

def handle_error(error):
    """Custom error handler."""
    return f"Sorry, an error occurred: {str(error)}"

def safe_process(input_data):
    try:
        # Your processing logic
        return process(input_data)
    except Exception as e:
        return handle_error(e)

safe_chain = (
    prompt
    | llm
    | StrOutputParser()
).with_fallbacks([
    RunnableLambda(lambda x: "Fallback response: Unable to process request")
])

Practical Examples

Example 1: Blog Post Generator

from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel

llm = ChatOpenAI(model="gpt-4", temperature=0.7)

# Step 1: Generate title and outline in parallel
title_prompt = ChatPromptTemplate.from_template(
    "Generate a catchy blog title about: {topic}"
)

outline_prompt = ChatPromptTemplate.from_template(
    "Create a 5-point outline for a blog post about: {topic}"
)

initial_chain = RunnableParallel(
    title=title_prompt | llm | StrOutputParser(),
    outline=outline_prompt | llm | StrOutputParser(),
    topic=lambda x: x["topic"]  # Pass through original topic
)

# Step 2: Write the full blog post
blog_prompt = ChatPromptTemplate.from_template("""
Write a blog post with:
Title: {title}
Topic: {topic}
Outline: {outline}

Write engaging content, about 500 words.
""")

# Step 3: Generate meta description
meta_prompt = ChatPromptTemplate.from_template(
    "Write a 160-character SEO meta description for this blog post:\n{blog_post}"
)

# Complete chain
blog_chain = (
    initial_chain
    | {
        "blog_post": blog_prompt | llm | StrOutputParser(),
        "title": lambda x: x["title"],
        "outline": lambda x: x["outline"]
    }
    | {
        "blog_post": lambda x: x["blog_post"],
        "title": lambda x: x["title"],
        "meta_description": meta_prompt | llm | StrOutputParser()
    }
)

result = blog_chain.invoke({"topic": "Benefits of meditation for developers"})

print(f"Title: {result['title']}")
print(f"Meta: {result['meta_description']}")
print(f"\n{result['blog_post']}")

Example 2: Document Q&A Chain

from langchain_core.runnables import RunnablePassthrough
from operator import itemgetter

# Simulated retriever (in real app, use vector database)
def retrieve_docs(question: str) -> str:
    # Simulated document retrieval
    docs = {
        "python": "Python is a high-level programming language created by Guido van Rossum.",
        "javascript": "JavaScript is a scripting language for web development.",
        "default": "No specific information found."
    }
    for key in docs:
        if key in question.lower():
            return docs[key]
    return docs["default"]

retriever = RunnableLambda(lambda x: retrieve_docs(x["question"]))

# RAG prompt
rag_prompt = ChatPromptTemplate.from_template("""
Answer the question based on the context below.

Context: {context}

Question: {question}

Answer:""")

# RAG chain
rag_chain = (
    RunnableParallel(
        context=retriever,
        question=itemgetter("question")
    )
    | rag_prompt
    | llm
    | StrOutputParser()
)

answer = rag_chain.invoke({"question": "Who created Python?"})
print(answer)  # "Python was created by Guido van Rossum."

Example 3: Multi-Language Translator

# Translate to multiple languages in parallel

translate_to_spanish = ChatPromptTemplate.from_template(
    "Translate to Spanish: {text}"
) | llm | StrOutputParser()

translate_to_french = ChatPromptTemplate.from_template(
    "Translate to French: {text}"
) | llm | StrOutputParser()

translate_to_german = ChatPromptTemplate.from_template(
    "Translate to German: {text}"
) | llm | StrOutputParser()

translate_to_japanese = ChatPromptTemplate.from_template(
    "Translate to Japanese: {text}"
) | llm | StrOutputParser()

# All translations run in parallel
multi_translate = RunnableParallel(
    original=itemgetter("text"),
    spanish=translate_to_spanish,
    french=translate_to_french,
    german=translate_to_german,
    japanese=translate_to_japanese
)

result = multi_translate.invoke({"text": "Hello, how are you today?"})

print(f"Original: {result['original']}")
print(f"Spanish: {result['spanish']}")
print(f"French: {result['french']}")
print(f"German: {result['german']}")
print(f"Japanese: {result['japanese']}")

LCEL Best Practices

  • Keep chains focused: Each chain should do one thing well
  • Use meaningful variable names: analysis_chain not chain1
  • Add fallbacks for production: Always have a backup plan
  • Use streaming for long responses: Better user experience
  • Parallelize when possible: Faster execution
  • Test components individually: Debug each step separately
  • Use type hints: Document expected inputs/outputs
# Example of well-structured chain

from typing import TypedDict

class BlogInput(TypedDict):
    topic: str
    tone: str
    length: str

class BlogOutput(TypedDict):
    title: str
    content: str
    meta: str

def create_blog_chain():
    """Create a blog generation chain with proper structure."""

    title_chain = title_prompt | llm | StrOutputParser()
    content_chain = content_prompt | llm | StrOutputParser()
    meta_chain = meta_prompt | llm | StrOutputParser()

    return (
        RunnableParallel(
            title=title_chain,
            content=content_chain,
        )
        | {
            "title": itemgetter("title"),
            "content": itemgetter("content"),
            "meta": meta_chain
        }
    ).with_fallbacks([
        RunnableLambda(lambda x: {
            "title": "Untitled",
            "content": "Content generation failed",
            "meta": ""
        })
    ])

# Usage
blog_chain = create_blog_chain()
result: BlogOutput = blog_chain.invoke({
    "topic": "AI",
    "tone": "professional",
    "length": "medium"
})

Quick Reference

┌────────────────────────────────────────────────────────────────┐
│                    LCEL CHEAT SHEET                            │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  BASIC CHAIN                                                   │
│  chain = prompt | llm | parser                                 │
│                                                                │
│  RUN METHODS                                                   │
│  chain.invoke({"key": "value"})      # Single, sync           │
│  chain.stream({"key": "value"})      # Streaming              │
│  chain.batch([{...}, {...}])         # Multiple inputs        │
│  await chain.ainvoke({...})          # Async                  │
│                                                                │
│  PARALLEL EXECUTION                                            │
│  RunnableParallel(a=chain1, b=chain2)                         │
│                                                                │
│  CONDITIONAL ROUTING                                           │
│  RunnableBranch((condition, chain), default_chain)            │
│                                                                │
│  PASS DATA THROUGH                                             │
│  RunnablePassthrough()               # Pass all input         │
│  itemgetter("field")                 # Extract field          │
│                                                                │
│  CUSTOM FUNCTIONS                                              │
│  RunnableLambda(my_function)                                  │
│  (lambda x: transform(x))            # Inline                 │
│                                                                │
│  ERROR HANDLING                                                │
│  chain.with_fallbacks([backup_chain])                         │
│  chain.with_retry(stop_after_attempt=3)                       │
│                                                                │
└────────────────────────────────────────────────────────────────┘

Master LangChain Development

Our Agentic AI program covers LCEL, chains, and advanced LangChain patterns with hands-on projects.

Explore Agentic AI Program

Related Articles