Implementation:CrewAIInc CrewAI Tool Usage And Hooks
Overview
Concrete tool execution orchestrator and hook decorators for monitoring and intercepting tool calls provided by the CrewAI framework.
Source Reference
- Repository: crewAIInc/crewAI
- Files:
lib/crewai/src/crewai/tools/tool_usage.py-- Lines L73-1052 (ToolUsageclass)lib/crewai/src/crewai/hooks/decorators.py-- Lines L203-300 (hook decorators)
Signatures
ToolUsage
ToolUsage(
tools_handler,
tools,
task,
function_calling_llm,
agent,
).use(calling, tool_string) -> str
The use() method is the main entry point. It receives the tool call information from the LLM output, resolves the tool, validates arguments, executes hooks, runs the tool, and returns the result.
@before_tool_call Decorator
@before_tool_call(
func=None,
*,
tools: list[str] | None = None,
agents: list[str] | None = None,
)
Registers a function as a before-tool-call hook. The decorated function receives a ToolCallHookContext and returns bool | None:
- Return
Falseto block the tool execution. - Return
TrueorNoneto allow execution to proceed.
@after_tool_call Decorator
@after_tool_call(
func=None,
*,
tools: list[str] | None = None,
agents: list[str] | None = None,
)
Registers a function as an after-tool-call hook. The decorated function receives a ToolCallHookContext (which includes the tool_result) and returns str | None:
- Return a
strto replace the tool's result with the returned value. - Return
Noneto keep the original result unchanged.
ToolCallHookContext
class ToolCallHookContext:
tool_name: str # Name of the tool being invoked
tool_input: dict # Mutable dict of tool arguments (can be modified in before hooks)
tool: BaseTool # The tool instance
agent: BaseAgent # The agent invoking the tool
task: Task # The current task
crew: Crew # The crew the agent belongs to
tool_result: str | None # The tool's output (only available in after hooks)
Fields
| Context Field | Available In | Mutable | Description |
|---|---|---|---|
tool_name |
before, after | No | The name of the tool being called |
tool_input |
before, after | Yes (before) | Dict of arguments; can be modified in before hooks to change inputs |
tool |
before, after | No | The BaseTool instance being invoked
|
agent |
before, after | No | The agent that triggered the tool call |
task |
before, after | No | The task context of the tool call |
crew |
before, after | No | The crew the agent belongs to |
tool_result |
after only | No | The string result returned by the tool (only populated in after hooks) |
Import
from crewai.hooks import before_tool_call, after_tool_call
How It Works
ToolUsage Execution Pipeline
The ToolUsage class orchestrates the complete tool invocation lifecycle:
- Parse the LLM's output to extract the tool name and arguments. The parser handles multiple output formats (JSON function calls, XML-style tool use, natural language).
- Resolve the tool by matching the parsed name against available tools. If no match is found, an error is returned to the LLM.
- Validate the arguments against the tool's
args_schema. Type coercion is attempted for minor mismatches (e.g., string to int). - Check usage limits: If
max_usage_countis set and the count has been reached, return an error. - Fire before hooks: Execute all registered
@before_tool_callhooks matching the tool and agent filters. If any hook returnsFalse, abort execution. - Execute the tool's
_run()method with the validated (and possibly hook-modified) arguments. - Fire after hooks: Execute all registered
@after_tool_callhooks. If any hook returns a string, that string replaces the tool result. - Return the final result string to the LLM for further reasoning.
Hook Registration
Hooks are registered globally using the @before_tool_call and @after_tool_call decorators. The optional tools and agents parameters act as filters:
tools=["search_web", "read_file"]: The hook only fires for tools with these names.agents=["Research Analyst"]: The hook only fires when this agent role invokes a tool.- No filters: The hook fires for every tool invocation by every agent.
Example
@before_tool_call for Logging
from crewai.hooks import before_tool_call
@before_tool_call
def log_tool_invocation(context):
"""Log every tool call for observability."""
print(
f"[TOOL CALL] Agent '{context.agent.role}' "
f"calling '{context.tool_name}' "
f"with args: {context.tool_input}"
)
# Return None to allow execution to proceed
return None
@before_tool_call for Blocking
from crewai.hooks import before_tool_call
@before_tool_call(tools=["delete_file"])
def block_dangerous_delete(context):
"""Block file deletion for system-critical paths."""
file_path = context.tool_input.get("file_path", "")
if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
print(f"[BLOCKED] Refusing to delete system file: {file_path}")
return False # Block execution
return True # Allow execution
@after_tool_call for Result Sanitization
from crewai.hooks import after_tool_call
import re
@after_tool_call(tools=["search_web", "scrape_website"])
def sanitize_pii(context):
"""Remove email addresses from tool results before returning to LLM."""
if context.tool_result:
sanitized = re.sub(
r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
'[EMAIL REDACTED]',
context.tool_result,
)
return sanitized # Replace result with sanitized version
return None # Keep original result
@after_tool_call for Result Truncation
from crewai.hooks import after_tool_call
@after_tool_call
def truncate_long_results(context):
"""Truncate tool results that exceed 10,000 characters."""
max_length = 10_000
if context.tool_result and len(context.tool_result) > max_length:
truncated = context.tool_result[:max_length]
return truncated + "\n\n[RESULT TRUNCATED]"
return None # Keep original result
Filtered Hook for Specific Agent
from crewai.hooks import before_tool_call
@before_tool_call(agents=["Junior Researcher"], tools=["execute_code"])
def restrict_junior_code_execution(context):
"""Prevent junior researcher agents from executing code."""
print(f"[POLICY] Code execution not permitted for role: {context.agent.role}")
return False # Block execution
Modifying Tool Inputs
from crewai.hooks import before_tool_call
@before_tool_call(tools=["search_web"])
def add_safety_filter(context):
"""Add a safety filter to all web search queries."""
original_query = context.tool_input.get("query", "")
context.tool_input["query"] = f"{original_query} safe:active"
return True # Allow execution with modified input
Principle Link
Principle:CrewAIInc_CrewAI_Tool_Execution_And_Monitoring
See Also
- Implementation:CrewAIInc_CrewAI_BaseTool_Schema -- The tool interface whose
_run()method is called during execution - Implementation:CrewAIInc_CrewAI_Tool_Assignment_Config -- How tools are made available before execution
- Implementation:CrewAIInc_CrewAI_Tool_Decorator_And_Classes -- The tool creation patterns that produce executable tools
- Heuristic:CrewAIInc_CrewAI_MCP_Timeout_And_Retry_Strategy