Hey everyone, Nina here from agntbox.com, and today we’re diving into a topic that’s been buzzing in my Slack channels and Twitter feed for the last couple of months: managing AI agent workflows. Specifically, I want to talk about LangChain Expression Language (LCEL) and why I think it’s become my go-to framework for building reliable, production-ready AI applications.
I know, I know. LangChain. Some of you just rolled your eyes. It’s had its ups and downs, its moments of being a bit… much. But honestly, LCEL, which started really hitting its stride a little over a year ago and has seen consistent improvements since, feels like a completely different beast. It’s less about abstract chains and more about clear, composable components. It’s the practical evolution I’ve been waiting for.
My Journey to LCEL: The Workflow Headache
Let me take you back a bit. For the past year, my day job (when I’m not blogging here) involves building AI-powered tools for a content marketing agency. We’re talking about everything from drafting social media posts to generating detailed article outlines based on client briefs. Early on, I was experimenting with all sorts of approaches: raw API calls, custom Python classes, even some early versions of other frameworks. And honestly, it was a mess.
Imagine this: I had a workflow that needed to take a client’s raw request, summarize it, extract key entities, then use those entities to generate a few different content ideas, each requiring a separate LLM call. Then, it had to evaluate those ideas against some internal criteria, pick the best one, and finally, expand on it. Sounds straightforward, right?
In practice, it looked like a tangled spaghetti of function calls, error handling scattered everywhere, and debugging sessions that made me want to pull my hair out. If one step failed, the whole thing crashed. If I wanted to swap out an LLM, I had to dig through multiple files. It was brittle, hard to read, and a nightmare to maintain.
I remember one specific Friday afternoon, about eight months ago, I was trying to debug why a specific prompt wasn’t getting the right output for a very niche client brief. It took me three hours just to trace the data flow through five different functions. That’s when I knew I needed a better way. I’d dabbled with earlier LangChain versions, found them a bit too opinionated for my taste at the time, but then I started seeing more chatter about LCEL. The idea of explicit inputs and outputs, and the pipe operator, really appealed to my developer brain.
What is LCEL, Really?
At its heart, LCEL is a way to compose “runnables” – these are essentially callable objects that can take an input, do something, and return an output. The magic comes from how you combine them, primarily using the pipe | operator, but also other methods like .map(), .batch(), and .with_config(). It’s inspired by Unix pipes and functional programming, making your AI workflows feel much more like data pipelines.
The key benefits for me have been:
- Clarity: The flow of data is explicit. You can look at a chain and immediately understand what’s happening.
- Modularity: Each runnable is a self-contained unit. You can swap out components easily.
- Debugging: With clear inputs and outputs for each step, tracing issues becomes significantly simpler.
- Parallelism & Batching: LCEL makes it surprisingly easy to run parts of your workflow concurrently or process multiple inputs at once.
- Streaming & Async: It naturally supports streaming outputs and asynchronous execution, which is crucial for responsive applications.
It’s not just about chaining LLM calls. You can chain anything: custom Python functions, prompt templates, retrievers, parsers – pretty much any piece of logic that takes an input and produces an output.
A Practical Example: The “Smart Summarizer”
Let’s walk through a simplified version of that problematic content summarization workflow I mentioned earlier, but built with LCEL. Our goal: take a long article, extract the main topic, generate a catchy title, and provide a concise summary. We’ll use a couple of different LLM calls and some custom logic.
Step 1: Setting Up Our Environment (Quick & Dirty)
First, you’ll need LangChain installed and an OpenAI API key (or whatever LLM provider you prefer). I usually set up my environment variables like this:
import os
from dotenv import load_dotenv
load_dotenv() # Load environment variables from .env file
# os.environ["OPENAI_API_KEY"] = "sk-..."
Step 2: Defining Our Components
We’ll need an LLM, some prompt templates, and a simple output parser. I’m using OpenAI’s chat models for this.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# Our LLM instance
llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
parser = StrOutputParser()
# Prompt for topic extraction
topic_prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert at identifying the main topic of an article."),
("user", "What is the primary topic of the following article?\n\n{article}")
])
# Prompt for title generation
title_prompt = ChatPromptTemplate.from_messages([
("system", "You are a creative content marketer. Generate a catchy, SEO-friendly title for an article about: {topic}"),
("user", "Generate 3 title options. Only output the titles, one per line.")
])
# Prompt for summary generation
summary_prompt = ChatPromptTemplate.from_messages([
("system", "You are a professional content summarizer. Condense the following article into a concise, 3-sentence summary, focusing on key takeaways."),
("user", "Summarize this article:\n\n{article}")
])
Step 3: Building the LCEL Chain
Now for the fun part – assembling our workflow. We want to:
- Take the raw article.
- Extract the main topic.
- Using the topic, generate titles.
- Separately, summarize the original article.
- Combine the topic, chosen title (we’ll just pick the first one for simplicity), and summary.
Here’s how we can build this with LCEL:
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
# --- Sub-chains for individual tasks ---
# 1. Topic Extraction Chain
topic_chain = topic_prompt | llm | parser
# 2. Title Generation Chain (takes 'topic' as input)
title_chain = title_prompt | llm | parser
# 3. Summary Generation Chain (takes 'article' as input)
summary_chain = summary_prompt | llm | parser
# --- Combining everything ---
# We need to run topic extraction and summary generation in parallel,
# as they both depend on the original 'article' but not on each other.
# We also need the topic for the title generation.
# Let's define a function to pick the first title from the output
def pick_first_title(titles_str: str) -> str:
return titles_str.strip().split('\n')[0] if titles_str else ""
# The main chain structure
full_workflow = RunnableParallel(
# First, let's extract the topic and also pass the original article through
# We use RunnablePassthrough to keep the 'article' available for later steps
initial_analysis=RunnableParallel(
topic=topic_chain,
article=RunnablePassthrough() # Keeps the original article for later use
),
# Now, generate the summary based on the original article
summary=summary_chain
) | {
"topic": lambda x: x["initial_analysis"]["topic"],
"article": lambda x: x["initial_analysis"]["article"],
"summary": lambda x: x["summary"],
# Now, use the extracted topic to generate titles
"titles": title_chain # This will receive `topic` implicitly from the previous step if named correctly, or we can explicitly map
# For explicit mapping:
# "titles": RunnablePassthrough.assign(
# topic=lambda x: x["initial_analysis"]["topic"]
# ) | title_chain
} | {
"topic": lambda x: x["topic"],
"summary": lambda x: x["summary"],
"chosen_title": lambda x: pick_first_title(x["titles"])
}
Okay, that full_workflow looks a bit more complex than a simple pipe, and that’s because we’re doing more than just a linear sequence. Let’s break down the key parts:
RunnableParallel: This is how you run multiple runnables concurrently, often with the same input. Here, it allows us to extract the topic AND pass through the original article AND generate a summary, all in parallel or conceptually in parallel from the initial input.RunnablePassthrough(): This is super useful. It simply passes its input through as its output. We use it to ensure the originalarticlecontent is available for the summarization step, even while other parts of the chain are working on extracting the topic.| { "key": runnable_or_lambda }: This is a dictionary mapping. It takes the output of the previous step and allows you to create a new dictionary of inputs for the next step. You can use lambdas to pick specific parts of the previous output or run another runnable. This is where we ensure thetitle_chaingets thetopicit needs.
Step 4: Running Our Chain
Let’s give it a test run with some dummy article content. I’ll use a snippet about recent developments in AI agents.
article_content = """
The field of AI agents has seen remarkable progress in 2026, with several key breakthroughs pushing the boundaries of autonomous decision-making and interaction. One of the most significant advancements comes from Google DeepMind's "Gemini Pro-X" project, which demonstrated human-level performance in complex, multi-step planning tasks across diverse domains, from scientific research to logistics optimization. This was achieved through a novel combination of reinforcement learning with large language models, allowing agents to not only understand natural language instructions but also to learn from environmental feedback in real-time.
Another notable development is the increasing adoption of "self-improving" agent architectures. Companies like Anthropic and OpenAI are experimenting with meta-learning frameworks where agents can reflect on their past performance, identify shortcomings, and autonomously update their internal models and strategies. This capability is expected to drastically reduce the need for constant human oversight and intervention, paving the way for truly independent AI systems.
However, these advancements also bring new challenges, particularly concerning ethical considerations and control. Regulators worldwide are scrambling to develop frameworks that can keep pace with the rapid evolution of AI agent capabilities. The debate around "agentic AI safety" is intensifying, focusing on ensuring alignment with human values and preventing unintended consequences from increasingly autonomous systems. Researchers are exploring methods like constitutional AI and verifiable performance metrics to address these concerns proactively.
"""
# Invoke the chain
result = full_workflow.invoke({"article": article_content})
print(f"Topic: {result['topic']}")
print(f"Chosen Title: {result['chosen_title']}")
print(f"Summary: {result['summary']}")
When I ran this just now, I got something like:
Topic: Recent advancements and challenges in AI agents, including Google DeepMind's Gemini Pro-X, self-improving architectures, and ethical considerations.
Chosen Title: AI Agents in 2026: Breakthroughs, Self-Improvement, and Ethical Challenges
Summary: The field of AI agents has made significant progress in 2026, with Google DeepMind's Gemini Pro-X demonstrating human-level planning and companies like Anthropic and OpenAI developing self-improving architectures. These advancements reduce human oversight but also raise new ethical and control challenges, prompting regulators to develop frameworks and researchers to explore safety methods. The debate on agentic AI safety is intensifying as autonomous systems evolve rapidly.
Pretty neat, right? And what’s crucial is that if I wanted to swap out gpt-4o for, say, Anthropic’s Claude 3.5 Sonnet, I’d only need to change the ChatOpenAI line to ChatAnthropic(model="claude-3-5-sonnet-20240620", temperature=0.3) (assuming I’ve set up the Anthropic API key), and the entire workflow would adapt. No digging through nested functions.
Advanced LCEL Features I Can’t Live Without
Beyond the basic piping, LCEL offers a few other gems that have become indispensable for me:
1. Debugging with .get_graph().print_ascii()
Remember my debugging nightmare? LCEL makes it so much easier. You can visualize your chain! This is a lifesaver for complex workflows.
full_workflow.get_graph().print_ascii()
This will print an ASCII art representation of your chain, showing the flow of data. It’s incredibly helpful for understanding what’s going where, especially with RunnableParallel and dictionary mappings.
2. Streaming Outputs
For user-facing applications, waiting for a full response can feel slow. LCEL runnables support streaming, which means you can get tokens back as they’re generated. Just use .stream() instead of .invoke():
# For a simpler chain that just returns text
# response = (topic_prompt | llm | parser).stream({"article": article_content})
# for chunk in response:
# print(chunk, end="", flush=True)
# For our complex workflow, streaming will yield intermediate states
# To stream the final result, you'd typically stream the final LLM call
# or structure your final output step to yield parts.
# Let's say we want to stream the summary:
summary_stream = (summary_prompt | llm | parser).stream({"article": article_content})
print("\nStreaming Summary:")
for s_chunk in summary_stream:
print(s_chunk, end="", flush=True)
print("\n")
3. Configuration with .with_config()
Need to add a specific tag to a log, set a timeout, or define a callback for a specific part of your chain? .with_config() lets you do that without polluting your main chain definition. This is great for observability.
# Example: Add a tag to the topic extraction step for better logging
topic_chain_with_config = topic_chain.with_config({"tags": ["topic_extraction", "critical_step"]})
# You could then integrate this configured chain into your main workflow
# full_workflow = ... | { "topic": topic_chain_with_config } | ...
Why LCEL Works for Me (and might for you)
After months of using LCEL daily, I’ve come to appreciate it not just as a library, but as a paradigm for building AI applications. It enforces a certain discipline that leads to more robust and maintainable code. The explicit nature of inputs and outputs, combined with the visual clarity of the pipe operator, cuts down on cognitive load significantly.
Before, I’d spend ages trying to figure out which function was responsible for a specific intermediate output. Now, with LCEL, I just look at the chain, and the data flow is right there. It feels like writing SQL queries for data transformation, but for AI logic. It’s functional, it’s composable, and most importantly, it’s practical for real-world application development.
I’ve seen it scale from simple one-off scripts to complex agentic systems that orchestrate multiple LLM calls, tool uses, and human feedback loops. The fact that it integrates so well with LangSmith for tracing and debugging is also a huge bonus, making production monitoring a lot less painful.
Actionable Takeaways
- Start Small: Don’t try to refactor your entire application at once. Pick one discrete AI workflow and try to implement it using LCEL.
- Think in Runnables: Break down your AI tasks into the smallest possible callable units. Each unit should have a clear input and output.
- Embrace the Pipe: Get comfortable with the
|operator. It’s the core of LCEL’s expressiveness. - Utilize
RunnableParalleland Dictionary Mappings: For anything beyond a simple linear flow, these are your best friends for managing parallel execution and shaping data for subsequent steps. - Visualize Your Chains: Use
.get_graph().print_ascii()often, especially when you’re learning or building complex chains. It makes debugging much easier. - Explore LangSmith: If you’re serious about production, integrate your LCEL chains with LangSmith. The tracing and evaluation features are invaluable.
So, if you’ve been on the fence about LangChain, or perhaps had a less-than-stellar experience with earlier versions, I urge you to take another look at LCEL. It has genuinely changed how I approach building AI applications, making them clearer, more robust, and a lot more fun to develop. Give it a try, and let me know your thoughts in the comments!
🕒 Published: