{"record":{"id":"3212c689f86725d1","repo":"langchain-ai/langchain","slug":"the-input-to-runnablepassthrough-assign-must-be","errorCode":null,"errorMessage":"The input to RunnablePassthrough.assign() must be a dict.","messagePattern":"The input to RunnablePassthrough\\.assign\\(\\) must be a dict\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"libs/core/langchain_core/runnables/passthrough.py","lineNumber":493,"sourceCode":"        # add passthrough node and edges\n        input_node = graph.first_node()\n        output_node = graph.last_node()\n        if input_node is not None and output_node is not None:\n            passthrough_node = graph.add_node(_graph_passthrough)\n            graph.add_edge(input_node, passthrough_node)\n            graph.add_edge(passthrough_node, output_node)\n        return graph\n\n    def _invoke(\n        self,\n        value: dict[str, Any],\n        run_manager: CallbackManagerForChainRun,\n        config: RunnableConfig,\n        **kwargs: Any,\n    ) -> dict[str, Any]:\n        if not isinstance(value, dict):\n            msg = \"The input to RunnablePassthrough.assign() must be a dict.\"  # type: ignore[unreachable]\n            raise ValueError(msg)  # noqa: TRY004\n\n        return {\n            **value,\n            **self.mapper.invoke(\n                value,\n                patch_config(config, callbacks=run_manager.get_child()),\n                **kwargs,\n            ),\n        }\n\n    @override\n    def invoke(\n        self,\n        input: dict[str, Any],\n        config: RunnableConfig | None = None,\n        **kwargs: Any,\n    ) -> dict[str, Any]:\n        return self._call_with_config(self._invoke, input, config, **kwargs)","sourceCodeStart":475,"sourceCodeEnd":511,"githubUrl":"https://github.com/langchain-ai/langchain/blob/e32fa9a52eab3b61ad7a45399bfde59b3e580fc4/libs/core/langchain_core/runnables/passthrough.py#L475-L511","documentation":"RunnablePassthrough.assign() copies the input dict and merges in the assigned keys, so its input must be a dict. The sync _invoke raises this ValueError when a non-dict flows in (the type-ignore comment shows static types already forbid it, so at runtime it means the previous step emitted a non-dict).","triggerScenarios":"Chaining .assign() after a runnable that returns a string or list (e.g. prompt | llm | RunnablePassthrough.assign(...)), or calling assign-wrapped chains directly with a string input.","commonSituations":"Putting assign after an LLM/chat model whose output is a message/string instead of before it; assuming assign passes through arbitrary payloads; mixing structured and unstructured steps in an LCEL chain.","solutions":["Reorder the chain so assign runs on dict data, e.g. RunnablePassthrough.assign(context=retriever) | prompt | llm","If the upstream returns a string, wrap it first (e.g. RunnableLambda(lambda s: {\"text\": s})) so assign receives a dict","Call .invoke({\"key\": value}) with a dict input directly"],"exampleFix":"# before\nchain = prompt | llm | RunnablePassthrough.assign(meta=lambda _: \"v\")\n# llm output is a message, not a dict -> ValueError\n# after\nchain = RunnablePassthrough.assign(meta=lambda _: \"v\") | prompt | llm","handlingStrategy":"type-guard","validationCode":"assert isinstance(value, dict), f\"assign needs dict input, got {type(value)}\"","typeGuard":"def is_dict_input(v: object) -> bool:\n    return isinstance(v, dict)","tryCatchPattern":"try:\n    chain.invoke(inputs)\nexcept ValueError as e:\n    if \"RunnablePassthrough.assign()\" in str(e):\n        chain.invoke({\"text\": str(inputs)})","preventionTips":["Place assign() before dict-destroying steps (llm, str parsers) in LCEL chains","Insert a RunnableLambda that re-wraps non-dict values into a dict before assign"],"tags":["lcel","runnable","passthrough-assign","type-mismatch","valueerror"],"backgroundTag":null,"analyzedSha":"e32fa9a52eab3b61ad7a45399bfde59b3e580fc4","analyzedAt":"2026-08-14T18:42:09.092Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}