deepset-ai/haystack · error
Hook function ' ' must take a single parameter annotated…
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.
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
- `async_function` must be a coroutine function defined with…
- Expected one ToolExecutionDecision for each tool call, but…
- `function` must be a synchronous function.
- No unused ToolExecutionDecision matches tool call
- A `ChatMessage` must contain at least one `TextContent`…
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/df99177728f561e0.
Report an issue: GitHub.
Appendix: 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)