LangChain RunnableParallel — dict output shape not matching downstream
<code>RunnableParallel</code> fans out to multiple branches and merges their results into a dict. The confusion begins when a downstream step expects one specific branch instead of the whole dict.
Quick fix (TL;DR)
RunnableParallel({"a": chain_a, "b": chain_b}) runs both branches on the same input and returns {"a": result_a, "b": result_b}. Downstream steps must consume that dict shape. Fix by (a) matching branch names to downstream prompt variables, (b) using itemgetter to extract single fields, and (c) piping into RunnablePassthrough.assign() for the classic RAG pattern where context and question travel together.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.
KeyError: 'context'
# RunnableParallel returned {"docs": [...], "question": "..."}
# but the prompt template expects {context} not {docs}TypeError: expected str or list, got dict
# Chained RunnableParallel | some_function — the function got the whole
# {a: ..., b: ...} dict when it wanted just one branch's resultasyncio.exceptions.CancelledError: One branch of RunnableParallel raised; all other branches were cancelled.
Reference
RunnableParallel shape flow
| Stage | Shape | Notes |
|---|---|---|
| Input to parallel | anything | Same input goes to every branch |
| Each branch | runs independently | Concurrent execution (async under the hood) |
| Merged output | dict | One key per branch, value is that branch's output |
| Downstream consumer | expects the dict | Or extract a field via itemgetter |
Common RunnableParallel patterns
| Pattern | Shape | Use case |
|---|---|---|
| Fan-out for retrieval + passthrough | {"context": docs, "question": q} | RAG |
| Multi-model comparison | {"gpt": ..., "claude": ...} | A/B model outputs |
| Multi-tool call in parallel | {"weather": ..., "news": ...} | Agent-style multi-source |
| Enrichment then merge | {"raw": input, "enriched": ...} | Data pipelines |
Root causes, ranked by frequency
Based on developer reports across LangChain forums, GitHub issues, and Discord community during 2025–2026.
- 28%Branch names do not match downstream prompt variables. Parallel emits
{"docs": ...}; prompt template uses{context}. - 18%Downstream step expects a value but gets a dict. Chained a scalar-consuming function after
RunnableParallel. - 14%Confusion with RunnablePassthrough.assign vs RunnableParallel.
.assign()ADDS keys to an existing dict;ParallelREPLACES the input with a new dict. - 10%One branch fails; async cancels others. Loss of partial results if branch reliability differs.
- 8%Nested Parallel emits dict-of-dicts. Not always what the developer expected.
- 7%Shorthand dict syntax not recognised. Some LangChain versions accept a bare dict as an implicit
RunnableParallel; others require the explicit constructor. - 7%Async branch raises but sync invoke swallows details. Traces show the parallel wrapper, hiding the underlying branch error.
- 8%Branch order not deterministic. When one branch depends on another's side effects (bad design but common), race conditions appear.
Fixes — copy-paste solutions
Match branch names to downstream prompt variables exactly
When the output of RunnableParallel feeds into a prompt template, branch names must exactly match the template variables. Rename branches or templates to align.
from operator import itemgetter from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnableParallel, RunnablePassthrough from langchain_core.output_parsers import StrOutputParser from langchain_openai import ChatOpenAI model = ChatOpenAI(model="gpt-4o-mini") # Prompt uses {context} and {question} prompt = ChatPromptTemplate.from_template( "Given this context:\n{context}\n\nAnswer: {question}" ) def retrieve(question: str) -> str: # Your vector store or search return "Relevant docs about " + question # ❌ WRONG — branch names do not match template variables # parallel_wrong = RunnableParallel(docs=retrieve, q=RunnablePassthrough()) # chain = parallel_wrong | prompt | model # KeyError: 'context' # ✓ RIGHT — branch names match {context} and {question} parallel = RunnableParallel( context=itemgetter("question") | RunnableLambda(retrieve), question=itemgetter("question"), # or RunnablePassthrough() if input is str ) chain = parallel | prompt | model | StrOutputParser() print(chain.invoke({"question": "What is LCEL?"})) # ✓ CLEANER — RunnablePassthrough.assign() adds context, keeps question chain2 = ( RunnablePassthrough.assign( context=lambda x: retrieve(x["question"]) ) | prompt | model | StrOutputParser() ) print(chain2.invoke({"question": "What is LCEL?"}))
RunnablePassthrough.assign() form is usually cleaner than explicit RunnableParallel for the "add a field to existing dict" pattern. Reserve Parallel for genuine fan-out to independent branches.Use itemgetter to extract single fields from parallel output
When you need one branch's result but the parallel emits many, use itemgetter("branch_name") as the next step to extract just that value.
from operator import itemgetter from langchain_core.runnables import RunnableParallel from langchain_openai import ChatOpenAI model = ChatOpenAI(model="gpt-4o-mini") # Fan out to two independent processes parallel = RunnableParallel( summary=summarise_chain, keywords=keywords_chain, ) # Downstream we only need the summary — extract with itemgetter summary_only_chain = parallel | itemgetter("summary") # For downstream that needs both, keep the dict shape both_chain = parallel | some_step_that_takes_dict # Combining multiple itemgetters def render(x): return f"Summary: {x['summary']}\n\nKeywords: {', '.join(x['keywords'])}" rendered_chain = parallel | render # When you need to reshape a dict: reshape_chain = ( parallel | (lambda x: {"final_summary": x["summary"], "final_tags": x["keywords"]}) ) # For very long-form transformations, use RunnableLambda for tracing benefits from langchain_core.runnables import RunnableLambda labeled_chain = parallel | RunnableLambda( lambda x: {"final_summary": x["summary"], "final_tags": x["keywords"]}, name="reshape_output", )
RunnableLambda — the name appears in LangSmith traces, making debugging much easier than seeing anonymous lambda steps.Handle partial failures in parallel branches
Use Runnable.with_fallbacks() per branch, or catch exceptions in the branch's wrapping lambda. LangChain 0.2+ also provides RunnableParallel configuration for error handling.
from langchain_core.runnables import RunnableParallel, RunnableLambda # Flaky retrieval — wrap it so failures return an empty result instead of throwing def safe_retrieve(question: str) -> str: try: return retrieve(question) except Exception as e: # Log to your observability stack print(f"[retrieve failed] {e}") return "" # graceful degradation — chain continues with empty context # Per-branch fallback using with_fallbacks from langchain_core.runnables import RunnableLambda primary_retrieve = RunnableLambda(retrieve) fallback_retrieve = RunnableLambda(lambda q: "") resilient_retrieve = primary_retrieve.with_fallbacks([fallback_retrieve]) parallel = RunnableParallel( context=itemgetter("question") | resilient_retrieve, question=itemgetter("question"), ) # If retrieve fails, context becomes "" and the chain still runs chain = parallel | prompt | model | StrOutputParser() # For async chains, be aware: one branch raising will cancel the others # unless you set return_exceptions=True in the underlying gather. # The simplest defense is to make each branch self-recovering as above.
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Match
RunnableParallelbranch names to downstream prompt template variables exactly. - Prefer
RunnablePassthrough.assign()when the intent is "add a field to a dict", not fan out. - Use
itemgetteror namedRunnableLambdato extract single fields cleanly. - Add
.with_fallbacks()to every unreliable branch (retrieval, external APIs). - Name every non-trivial lambda so LangSmith traces are readable.
- For nested Parallel, sketch the shape at each depth before writing code — dict-of-dicts confuses fast.
- Test parallel chains with a "one branch always fails" test case; verify graceful degradation.
Frequently asked questions
asyncio.gather under the hood. Sync invoke uses a thread pool. For CPU-bound branches this gives near-linear speedup; for I/O-bound branches (typical for RAG) the benefit is dominant..assign() when later steps need earlier results. Sequential is easier to read; parallel is faster.RunnableParallel. Example: {"context": retrieve, "question": passthrough} | prompt | model. Explicit is clearer but the sugar is idiomatic.RunnableParallel's max_concurrency config or batch the work with abatch().Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.