deepset-ai/haystack · error · ValueError

outputs_to_state handler for key '{key}' must be callable

Error message

outputs_to_state handler for key '{key}' must be callable

What it means

When an outputs_to_state entry includes a "handler" key, its value must be callable — a function invoked to process the output before storing it. __post_init__ raises ValueError if "handler" exists but is not callable (string, None, dict, etc.).

Source

Thrown at haystack/tools/tool.py:145

                f"`async_function` must be a coroutine function defined with `async def`. "
                f"Got '{getattr(self.async_function, '__name__', repr(self.async_function))}'."
            )

        # Check that the parameters define a valid JSON schema
        try:
            Draft202012Validator.check_schema(self.parameters)
        except SchemaError as e:
            raise ValueError("The provided parameters do not define a valid JSON schema") from e

        # Validate outputs structure if provided
        if self.outputs_to_state is not None:
            for key, config in self.outputs_to_state.items():
                if not isinstance(config, dict):
                    raise TypeError(f"outputs_to_state configuration for key '{key}' must be a dictionary")
                if "source" in config and not isinstance(config["source"], str):
                    raise ValueError(f"outputs_to_state source for key '{key}' must be a string.")
                if "handler" in config and not callable(config["handler"]):
                    raise ValueError(f"outputs_to_state handler for key '{key}' must be callable")

            # Validate that outputs_to_state source keys exist as valid tool outputs
            valid_outputs: set[str] | None = self._get_valid_outputs()
            if valid_outputs is not None:
                for state_key, config in self.outputs_to_state.items():
                    source = config.get("source")
                    if source is not None and source not in valid_outputs:
                        raise ValueError(
                            f"outputs_to_state: '{self.name}' maps state key '{state_key}' to unknown output '{source}'"
                            f"Valid outputs are: {valid_outputs}."
                        )

        if self.outputs_to_string is not None:
            if "source" in self.outputs_to_string and not isinstance(self.outputs_to_string["source"], str):
                raise ValueError("outputs_to_string source must be a string.")
            if "handler" in self.outputs_to_string and not callable(self.outputs_to_string["handler"]):
                raise ValueError("outputs_to_string handler must be callable")
            if "raw_result" in self.outputs_to_string and not isinstance(self.outputs_to_string["raw_result"], bool):

View on GitHub (pinned to e318778c9b)

Solutions

  1. Pass the actual callable: {"k": {"handler": my_handler}} with my_handler imported/defined.
  2. If configuration comes from strings, resolve to callables via a registry dict before constructing the Tool.
  3. Remove the "handler" key if no transformation is needed.

Example fix

// before
outputs_to_state={"k": {"handler": "format_doc"}}

// after
from mymod import format_doc
outputs_to_state={"k": {"handler": format_doc}}
Defensive patterns

Strategy: type-guard

Validate before calling

for k, cfg in outputs_to_state.items():
    if "handler" in cfg and not callable(cfg["handler"]):
        raise TypeError(f"handler for '{k}' must be callable")

Type guard

from collections.abc import Callable

def has_valid_handlers(ots: dict) -> bool:
    return all(callable(c.get("handler", lambda: None)) for c in ots.values())

Try / catch

try:
    tool = Tool(name="t", function=f, outputs_to_state=ots)
except ValueError as e:
    if "handler" in str(e):
        logger.error(f"Resolve handler callables: {e}")
    raise

Prevention

When it happens

Trigger: Tool(..., outputs_to_state={"k": {"handler": "my_func_name"}}) or {"k": {"handler": None}} — passing a function name/placeholder instead of the function object.

Common situations: Passing a string function name hoping for late resolution; forgetting to import the handler so a variable holds the wrong object; YAML/JSON-driven config where handlers are serialized as strings.

Related errors


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/9436c4ffe2a12883. Report an issue: GitHub.