deepset-ai/haystack · error

Hook function '{func.__name__}' must take a single parameter

Error message

Hook function '{func.__name__}' must take a single parameter annotated with `State` (e.g. `def my_hook(state: State) -> None`).

What it means

FunctionHook hook functions must declare exactly one parameter annotated with `State`, because the Agent injects its live State as the sole argument. Any sync or async function supplied whose signature does not match this contract raises ValueError at construction time.

Source

Thrown at haystack/hooks/from_function.py:63

            `run` raises a `RuntimeError`. When only `function` is set, `run_async` calls `function`.
        :raises ValueError: If neither is set, if `function` is a coroutine function, if `async_function` is not, or
            if a provided function does not declare a `State`-typed parameter.
        """
        if function is None and async_function is None:
            raise ValueError("A FunctionHook requires at least one of `function` or `async_function` to be set.")
        if function is not None and inspect.iscoroutinefunction(function):
            raise ValueError(
                f"`function` must be a synchronous function. '{function.__name__}' is a coroutine function. "
                "Pass it as `async_function` instead."
            )
        if async_function is not None and not inspect.iscoroutinefunction(async_function):
            raise ValueError(
                f"`async_function` must be a coroutine function defined with `async def`. "
                f"Got '{getattr(async_function, '__name__', repr(async_function))}'."
            )
        for func in (function, async_function):
            if func is not None and not _takes_single_state_argument(func):
                raise ValueError(
                    f"Hook function '{func.__name__}' must take a single parameter annotated with `State` "
                    "(e.g. `def my_hook(state: State) -> None`)."
                )
        self.function = function
        self.async_function = async_function

    def run(self, state: State) -> None:
        """
        Run the synchronous function against the live `State`.

        :param state: The Agent's live `State`, mutated in place by the wrapped function.
        :raises RuntimeError: If the hook only has an `async_function`; use the Agent's async run methods instead.
        """
        if self.function is None:
            raise RuntimeError(
                "This FunctionHook only has an `async_function` and cannot run in a synchronous Agent run. "
                "Use the Agent's async run methods, or provide a synchronous `function`."
            )

View on GitHub (pinned to e318778c9b)

Solutions

  1. Change the hook signature to exactly `def my_hook(state: State) -> None` (or `async def`)
  2. Ensure `State` is the real class imported from haystack (not a string alias or wrong type)
  3. If extra context is needed, close over it instead of adding parameters

Example fix

// before
def my_hook(state, extra_ctx): ...
hook = FunctionHook(function=my_hook)  # ValueError
// after
from haystack import State
def my_hook(state: State) -> None: ...
hook = FunctionHook(function=my_hook)
Defensive patterns

Strategy: validation

Validate before calling

import inspect
from haystack import State
sig = inspect.signature(fn)
params = list(sig.parameters.values())
assert len(params) == 1 and params[0].annotation is State, "hook must take one State-annotated param"

Type guard

import inspect
from typing import get_type_hints
from haystack import State
def is_state_hook(fn) -> bool:
    params = list(inspect.signature(fn).parameters.values())
    if len(params) != 1:
        return False
    return get_type_hints(fn).get(params[0].name) is State

Try / catch

try:
    hook = FunctionHook(function=fn)
except ValueError as e:
    raise TypeError(f"hook signature invalid: {e}") from e

Prevention

When it happens

Trigger: Passing to `function`/`async_function` a callable with zero parameters, multiple parameters, or a parameter not annotated as `State`, detected by _takes_single_state_argument.

Common situations: Writing hooks like `def my_hook(state)` without the annotation; reusing an existing callback with extra kwargs; typos in the State import so the annotation is a different type.

Related errors


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