LangChain RunnableSequence — input schema mismatch between steps
LCEL is compositional but strict about shapes. Every arrow in a pipe passes structured data — when one runnable emits <code>{"answer": "..."}</code> and the next expects a bare string, the chain crashes with a type error deep in the trace.
Quick fix (TL;DR)
RunnableSequence (created with the | operator) pipes each step's output as the next step's input. Failure modes: (a) a prompt template expects a dict with specific keys but gets a raw string; (b) a chat model emits an AIMessage that the next parser cannot handle; (c) parallel branches emit differently-shaped dicts. Fix by (a) using RunnablePassthrough.assign() to thread inputs through, (b) using itemgetter or lambdas to extract fields, and (c) inspecting chain.input_schema / chain.output_schema.Real error messages you'll see
These are the exact strings returned by the LangChain framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.
Traceback (most recent call last):
File "chain.py", line 42, in <module>
chain.invoke("what is the capital of France")
...
File ".../prompts/chat.py", line 385, in format_messages
return self.format_prompt(**kwargs).to_messages()
File ".../prompts/prompt.py", line 155, in format
return self.template.format(**kwargs)
KeyError: 'question'
# The prompt template expected {"question": "..."} but got a bare stringTypeError: Expected str, got AIMessage # The parser needs .content extracted from the AIMessage first, # or use StrOutputParser between model and parser
ValidationError: 1 validation error for MyOutput field required (type=value_error.missing)
Reference
Common shape transitions in LCEL chains
| Step type | Input shape | Output shape |
|---|---|---|
ChatPromptTemplate | dict with template vars | PromptValue |
ChatModel | PromptValue or list of messages | AIMessage |
StrOutputParser | AIMessage or str | str |
JsonOutputParser | AIMessage or str | dict |
PydanticOutputParser | str | Pydantic model |
RunnablePassthrough | anything | same anything |
RunnablePassthrough.assign() | dict | dict with new keys added |
RunnableParallel | anything | dict with one key per branch |
Root causes, ranked by frequency
Based on developer reports across LangChain forums, GitHub issues, and Discord community during 2025–2026.
- 26%Prompt expects dict, receives bare string. Common when chaining
str | prompt | modelinstead of wrapping the string in{"question": ...}. - 18%Missing StrOutputParser between model and downstream text step. The next step expects a string but gets an
AIMessage. - 14%RunnableParallel branch names not matching downstream keys. Branch outputs
{"a": ..., "b": ...}; next step needs{"context": ..., "question": ...}. - 10%Pydantic output parser schema drift. Parser expects fields the model did not emit; validation fails silently as generic KeyError.
- 8%Async / sync mismatch. Using
ainvokeon a chain whose middle step has only a sync implementation forces sync coercion, occasionally shape-warping. - 7%Config / RunnableConfig not passed through. Custom runnable eats the
configarg; downstream callbacks / tracing break, sometimes surfacing as type errors. - 7%Chained with an incompatible retriever.
RetrieverreturnsList[Document]; next step expects a formatted string. - 10%Version drift between langchain-core and integrations. Newer
langchain-coretightens type checks; older integration packages produce loose outputs that no longer validate.
Fixes — copy-paste solutions
Wrap raw inputs with a dict-shaping runnable at the top of the chain
When the user calls chain.invoke("some question"), you almost always want that string mapped into {"question": "..."} for the prompt template. Use a lambda, RunnablePassthrough, or a helper.
from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough, RunnableLambda from langchain_core.output_parsers import StrOutputParser from langchain_openai import ChatOpenAI model = ChatOpenAI(model="gpt-4o-mini") prompt = ChatPromptTemplate.from_messages([ ("system", "You answer briefly."), ("human", "{question}"), ]) # ❌ WRONG — bare string flows into prompt that expects a dict # chain = prompt | model | StrOutputParser() # chain.invoke("what is the capital of France") # KeyError: 'question' # ✓ RIGHT — shape the input first chain = ( RunnableLambda(lambda x: {"question": x} if isinstance(x, str) else x) | prompt | model | StrOutputParser() ) print(chain.invoke("what is the capital of France")) # > Paris # ✓ ALTERNATIVE — call invoke with a dict directly chain2 = prompt | model | StrOutputParser() print(chain2.invoke({"question": "what is the capital of France"})) # ✓ BEST for retrieval workflows — RunnablePassthrough.assign retrieval_chain = ( RunnablePassthrough.assign( # add 'context' key derived from 'question' key, keeping 'question' context=lambda x: retrieve_docs(x["question"]) ) | prompt_with_context | model | StrOutputParser() )
chain.input_schema.model_json_schema() to see what the chain expects.Insert StrOutputParser wherever text is expected
Chat models emit AIMessage objects, not plain strings. Downstream string operations (format templates, regex, further prompts) need the .content extracted. StrOutputParser does exactly this and belongs after almost every chat model call.
from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser, JsonOutputParser from langchain_openai import ChatOpenAI model = ChatOpenAI(model="gpt-4o-mini") # --- Multi-stage chain: research → refine --- research_prompt = ChatPromptTemplate.from_template( "Research: {topic}. List 5 key facts." ) refine_prompt = ChatPromptTemplate.from_template( "Given these research notes, write a 2-paragraph brief:\n\n{notes}" ) # ❌ WRONG — research emits AIMessage, refine_prompt.format wants a string # chain = research_prompt | model | (lambda ai: {"notes": ai}) | refine_prompt | model # TypeError: expected str, got AIMessage # ✓ RIGHT — StrOutputParser between the two model calls chain = ( research_prompt | model | StrOutputParser() # AIMessage -> str | (lambda notes: {"notes": notes}) # str -> dict for next prompt | refine_prompt | model | StrOutputParser() # final AIMessage -> str ) result = chain.invoke({"topic": "carbon capture"}) print(result) # --- For structured output, use JsonOutputParser or with_structured_output --- structured_chain = ( ChatPromptTemplate.from_template("Extract as JSON: {text}") | model | JsonOutputParser() # AIMessage -> dict )
StrOutputParser in dev and add it when the chain first breaks. Add it up front instead — every hop where a model output flows into text-manipulation code needs a parser.Debug shape errors with input_schema and get_graph
Every LCEL runnable has .input_schema, .output_schema, and .get_graph(). Use these to introspect what each step expects before invoke-time.
from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser from langchain_openai import ChatOpenAI prompt = ChatPromptTemplate.from_template("Explain {concept} to a {audience}") model = ChatOpenAI(model="gpt-4o-mini") chain = prompt | model | StrOutputParser() # 1) What does the WHOLE chain expect / emit? print(chain.input_schema.model_json_schema()) # {'title': 'PromptInput', 'type': 'object', # 'properties': {'concept': {'type': 'string'}, 'audience': {'type': 'string'}}, # 'required': ['concept', 'audience']} print(chain.output_schema.model_json_schema()) # {'type': 'string', 'title': 'StrOutputParserOutput'} # 2) What does each STEP in the chain expect / emit? graph = chain.get_graph() print(graph.draw_ascii()) # visual pipeline print(graph.draw_mermaid()) # mermaid syntax for docs # 3) Try invoking with the schema in mind chain.invoke({"concept": "gradient descent", "audience": "10-year-old"}) # 4) When a chain fails, isolate the failing hop # Invoke each prefix separately: step1 = prompt.invoke({"concept": "x", "audience": "y"}) print(type(step1)) # ChatPromptValue step2 = model.invoke(step1) print(type(step2)) # AIMessage step3 = StrOutputParser().invoke(step2) print(type(step3)) # str
chain.input_schema and chain.output_schema for each significant chain in the codebase. It doubles as living documentation.Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Always start LCEL chains with a shape-setting step; never pipe a bare string into a prompt template.
- Insert
StrOutputParser()after every chat model call whose output flows into text-manipulation code. - Use
RunnablePassthrough.assign()to thread values through the chain without losing them. - Inspect
chain.input_schemaandchain.output_schemaas part of unit tests. - Pin
langchain-coreand integration package versions inpyproject.toml— schema tightness varies between minor versions. - Prefer
with_structured_output()overJsonOutputParserwhen the output is structured — cleaner error surfaces. - Type-hint your custom
RunnableLambdafunctions so IDE catches shape mismatches at write time.
Frequently asked questions
RunnablePassthrough passes its input through unchanged. RunnablePassthrough.assign(k=fn) adds new key(s) computed from the input to a dict, keeping existing keys. The assign form is essential for building retrieval chains where you need both the original question and the retrieved context.RunnableGenerator that yields chunks.LLMChain, SequentialChain, and related classes are deprecated in current LangChain and will be removed. LCEL offers better streaming, async, and observability.Runnable[InputType, OutputType] as the annotation. For dict inputs, use TypedDict or Pydantic models. Example: chain: Runnable[QuestionInput, str] = prompt | model | StrOutputParser().FakeListChatModel for deterministic tests.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.