deepset-ai/haystack · error · ValueError
Hook of type ' ' is registered under hook point ' ' but…
Error message
Hook of type '{type(h).__name__}' is registered under hook point '{hook_point}' but only supports: {', '.join(allowed_points)}. What it means
During warm_up, every skill .md file's frontmatter must contain a non-empty 'description'. This ValueError is raised when frontmatter.get("description") is missing, None, or an empty string. The description is required because it is surfaced to the model for skill selection.
Solutions
- Add a non-empty 'description: ...' line to the skill file's frontmatter.
- If the file isn't meant to be a skill, move it out of skills_dir or into a subdirectory the scanner doesn't treat as a skill .md.
- Check the exact file named in the error message and confirm the key spelling is 'description' (lowercase).
Example fix
// before (SKILL.md) --- name: my-skill --- // after --- name: my-skill description: Explains how to use the my-skill workflow. ---
Defensive patterns
Strategy: validation
Validate before calling
import yaml
from pathlib import Path
def skill_has_description(skill_file: Path) -> bool:
lines = skill_file.read_text(encoding="utf-8").splitlines()
closing = lines.index("---", 1)
fm = yaml.safe_load("\n".join(lines[1:closing])) or {}
return isinstance(fm, dict) and bool(fm.get("description")) Type guard
def has_nonempty_description(frontmatter: dict) -> bool:
return bool(frontmatter.get("description")) Try / catch
try:
store.list_skills()
except ValueError as e:
if "missing a 'description'" in str(e):
logger.error("Add a description to the frontmatter of: %s", e)
raise Prevention
- Start every new skill from a template that includes name and description
- Add a CI test asserting every skills/**/*.md frontmatter has a non-empty description
- Never remove or rename the 'description' key when editing skills
When it happens
Trigger: A skill markdown file in skills_dir whose YAML frontmatter lacks a 'description' key, has description: (empty/null), or description: "" ; triggered by warm_up via list_skills/load_skill or store construction-time warming.
Common situations: Authoring a new SKILL.md and forgetting the description field; stripping frontmatter keys with a linter/formatter; copying a skill template with placeholder removed; renaming keys (e.g. 'summary') instead of 'description'.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
- At least one of row_split_threshold or…
- 'chat_generators' must be a non-empty list
- 'dimension' must be a positive integer.
- 'dimension' must be a positive integer.
- Hook registered for hook point
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/9cfdffa59906dd0c.
Report an issue: GitHub.
Appendix: source
Thrown at haystack/components/agents/agent.py:141
raise ValueError(
f"Invalid hook point '{hook_point}'. Valid hook points are: {', '.join(VALID_HOOK_POINTS)}."
)
for h in hook_list:
if not callable(getattr(h, "run", None)):
if callable(h):
raise TypeError(
f"Hook registered for hook point '{hook_point}' is callable but is not a Hook object. "
"If it is a function, wrap it with the @hook decorator."
)
raise TypeError(
f"Hook registered for hook point '{hook_point}' must have a callable 'run(state)', "
f"got an object of type '{type(h).__name__}'."
)
# A hook may declare `allowed_hook_points` to restrict where it can run (e.g. ConfirmationHook only
# makes sense at "before_tool"). Hooks without it can be registered under any hook point.
allowed_points = getattr(h, "allowed_hook_points", None)
if allowed_points is not None and hook_point not in allowed_points:
raise ValueError(
f"Hook of type '{type(h).__name__}' is registered under hook point '{hook_point}' but only "
f"supports: {', '.join(allowed_points)}."
)
def _consume_continue_run(state: State) -> bool:
"""Return the `continue_run` control flag and reset it so it does not carry over to the next exit attempt."""
should_continue = state.data["continue_run"]
state.set("continue_run", False)
return should_continue
def _get_model_exit_reason(messages: list[ChatMessage]) -> str | None:
"""
Return the exit reason for a terminal assistant reply without tool calls.
Incomplete generation reasons take precedence over text so callers can distinguish a partial response from a
complete answer. An empty response without a recognized terminal reason does not trigger an exit, preserving theView on GitHub (pinned to e318778c9b)