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
- Change the hook signature to exactly `def my_hook(state: State) -> None` (or `async def`)
- Ensure `State` is the real class imported from haystack (not a string alias or wrong type)
- 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
- Use the canonical signature: def hook(state: State) -> None
- Import State from haystack so the annotation matches exactly
- Close over extra context instead of adding parameters
- Type-check hooks with mypy
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
- `function` must be a synchronous function. '{function.__name
- `async_function` must be a coroutine function defined with `
- Expected one ToolExecutionDecision for each tool call, but r
- No unused ToolExecutionDecision matches tool call {tc.tool_n
- Tool execution requires at least one tool.
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/df99177728f561e0.
Report an issue: GitHub.