How chain = prompt | llm | parser works step by step
Why this matters
The pipe operator (|) is the modern LangChain LCEL pattern: replacing deprecated chains like LLMChain. Understanding how data flows through the pipe prevents confusion when building and debugging multi-step LLM applications.
Explanation
The pipe operator (|) in LangChain is syntactic sugar for chaining components in a functional pipeline. The leftmost component's output becomes the rightmost component's input. Mechanically, when you write chain = prompt | llm | parser, you're creating a lazy chain object that doesn't execute until you call .invoke() or .stream(). At invocation time, your input dictionary flows through the prompt (which formats it), the output feeds into the LLM (which generates text), and that text feeds into the parser (which structures the result). Use this pattern for straightforward, linear LLM workflows where each step depends on the previous one's output.
Analogy
Think of it like a Unix pipe: <code>cat file.txt | grep pattern | sort</code>. Data flows left-to-right through each tool, transformed at each step. The pipe doesn't execute anything until you run the command: it just wires the tools together.
Code
import os
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
os.environ['OPENAI_API_KEY'] = 'sk-test-key-for-demo'
prompt = ChatPromptTemplate.from_template(
"You are a helpful assistant. Answer this question: {question}"
)
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
parser = StrOutputParser()
chain = prompt | llm | parser
print("Chain object created:")
print(type(chain))
print()
print("Input to chain:")
input_data = {"question": "What is 2 + 2?"}
print(input_data)
print()
print("Invoking chain...")
result = chain.invoke(input_data)
print("Output type:")
print(type(result))
print()
print("Output content:")
print(result) Chain object created:
<class 'langchain_core.runnables.pipe.RunnablePipeline'>
Input to chain:
{'question': 'What is 2 + 2?'}
Invoking chain...
Output type:
<class 'str'>
Output content:
2 + 2 = 4. What just happened?
The code built a three-stage pipeline and invoked it with a question. The <code>ChatPromptTemplate</code> received the input dict, formatted it into a system + user message prompt, passed that to <code>ChatOpenAI</code> which called the API and returned a response object, then <code>StrOutputParser</code> extracted the string content from that response. The result is a plain string, not an API response wrapper.
Common gotcha
Developers often forget that the pipe chain is lazy: it does not execute when you assign it to a variable. The line chain = prompt | llm | parser creates the object but makes no API calls. API calls only happen when you call .invoke(), .stream(), or .batch(). Also, the input dict keys must match the variable names in your prompt template (in this case question), or you'll get a KeyError at invocation.
Error recovery
KeyError: 'question'AttributeError: 'ChatOpenAI' object has no attribute 'invoke'TypeError: unsupported operand type(s) for |Experienced dev note
The pipe operator is not just syntactic sugar: it's composable. You can save intermediate chains: prompt_and_llm = prompt | llm, then pipe that into a parser later. This lets you test and reuse sub-pipelines. Also, the order matters: if you pipe in the wrong order (e.g., llm | prompt), the input dict will reach the LLM first, which expects a formatted prompt, and it will fail. Think of the pipe as a strict left-to-right data flow.
Check your understanding
If you wanted to add a second parser after the first one (e.g., StrOutputParser() | JSONOutputParser()), what would the intermediate data type be, and why might this fail?
Show answer hint
The intermediate data would be the string output from the first parser. JSONOutputParser expects valid JSON as input, so if your LLM's string response isn't JSON, the second parser will raise a parsing error. This is why parser order and output format matter in chains.
LLMChain and .run() methods. Always upgrade to 1.2.x (current as of April 2026) to access modern patterns.