Execution Hooks Overview
Execution Hooks provide fine-grained control over the runtime behavior of your CrewAI agents. Unlike kickoff hooks that run before and after crew execution, execution hooks intercept specific operations during agent execution, allowing you to modify behavior, implement safety checks, and add comprehensive monitoring.
Types of Execution Hooks
Section titled “Types of Execution Hooks”CrewAI provides two main categories of execution hooks:
Control and monitor language model interactions:
- Before LLM Call: Modify prompts, validate inputs, implement approval gates
- After LLM Call: Transform responses, sanitize outputs, update conversation history
Use Cases:
- Iteration limiting
- Cost tracking and token usage monitoring
- Response sanitization and content filtering
- Human-in-the-loop approval for LLM calls
- Adding safety guidelines or context
- Debug logging and request/response inspection
View LLM Hooks Documentation →
Control and monitor tool execution:
- Before Tool Call: Modify inputs, validate parameters, block dangerous operations
- After Tool Call: Transform results, sanitize outputs, log execution details
Use Cases:
- Safety guardrails for destructive operations
- Human approval for sensitive actions
- Input validation and sanitization
- Result caching and rate limiting
- Tool usage analytics
- Debug logging and monitoring
View Tool Hooks Documentation →
Hook Registration Methods
Section titled “Hook Registration Methods”1. Decorator-Based Hooks (Recommended)
Section titled “1. Decorator-Based Hooks (Recommended)”The cleanest and most Pythonic way to register hooks:
from crewai.hooks import before_llm_call, after_llm_call, before_tool_call, after_tool_call
@before_llm_calldef limit_iterations(context): """Prevent infinite loops by limiting iterations.""" if context.iterations > 10: return False # Block execution return None
@after_llm_calldef sanitize_response(context): """Remove sensitive data from LLM responses.""" if "API_KEY" in context.response: return context.response.replace("API_KEY", "[REDACTED]") return None
@before_tool_calldef block_dangerous_tools(context): """Block destructive operations.""" if context.tool_name == "delete_database": return False # Block execution return None
@after_tool_calldef log_tool_result(context): """Log tool execution.""" print(f"Tool {context.tool_name} completed") return None2. Crew-Scoped Hooks
Section titled “2. Crew-Scoped Hooks”Apply hooks only to specific crew instances:
from crewai import CrewBasefrom crewai.project import crewfrom crewai.hooks import before_llm_call_crew, after_tool_call_crew
@CrewBaseclass MyProjCrew: @before_llm_call_crew def validate_inputs(self, context): # Only applies to this crew print(f"LLM call in {self.__class__.__name__}") return None
@after_tool_call_crew def log_results(self, context): # Crew-specific logging print(f"Tool result: {context.tool_result[:50]}...") return None
@crew def crew(self) -> Crew: return Crew( agents=self.agents, tasks=self.tasks, process=Process.sequential )Hook Execution Flow
Section titled “Hook Execution Flow”LLM Call Flow
Section titled “LLM Call Flow”Agent needs to call LLM ↓[Before LLM Call Hooks Execute] ├→ Hook 1: Validate iteration count ├→ Hook 2: Add safety context └→ Hook 3: Log request ↓If any hook returns False: ├→ Block LLM call └→ Raise ValueError ↓If all hooks return True/None: ├→ LLM call proceeds └→ Response generated ↓[After LLM Call Hooks Execute] ├→ Hook 1: Sanitize response ├→ Hook 2: Log response └→ Hook 3: Update metrics ↓Final response returnedTool Call Flow
Section titled “Tool Call Flow”Agent needs to execute tool ↓[Before Tool Call Hooks Execute] ├→ Hook 1: Check if tool is allowed ├→ Hook 2: Validate inputs └→ Hook 3: Request approval if needed ↓If any hook returns False: ├→ Block tool execution └→ Return error message ↓If all hooks return True/None: ├→ Tool execution proceeds └→ Result generated ↓[After Tool Call Hooks Execute] ├→ Hook 1: Sanitize result ├→ Hook 2: Cache result └→ Hook 3: Log metrics ↓Final result returnedHook Context Objects
Section titled “Hook Context Objects”LLMCallHookContext
Section titled “LLMCallHookContext”Provides access to LLM execution state:
class LLMCallHookContext: executor: CrewAgentExecutor # Full executor access messages: list # Mutable message list agent: Agent # Current agent task: Task # Current task crew: Crew # Crew instance llm: BaseLLM # LLM instance iterations: int # Current iteration response: str | None # LLM response (after hooks)ToolCallHookContext
Section titled “ToolCallHookContext”Provides access to tool execution state:
class ToolCallHookContext: tool_name: str # Tool being called tool_input: dict # Mutable input parameters tool: CrewStructuredTool # Tool instance agent: Agent | None # Agent executing task: Task | None # Current task crew: Crew | None # Crew instance tool_result: str | None # Agent-facing result string (after hooks) raw_tool_result: Any | None # Raw Python result (after hooks)For typed tool outputs, tool_result is the string the agent sees. By default, this is JSON. If the tool uses custom formatting, it can be Markdown or another string. raw_tool_result is the original Python value returned by the tool.
Common Patterns
Section titled “Common Patterns”Safety and Validation
Section titled “Safety and Validation”@before_tool_calldef safety_check(context): """Block destructive operations.""" dangerous = ['delete_file', 'drop_table', 'system_shutdown'] if context.tool_name in dangerous: print(f"🛑 Blocked: {context.tool_name}") return False return None
@before_llm_calldef iteration_limit(context): """Prevent infinite loops.""" if context.iterations > 15: print("⛔ Maximum iterations exceeded") return False return NoneHuman-in-the-Loop
Section titled “Human-in-the-Loop”@before_tool_calldef require_approval(context): """Require approval for sensitive operations.""" sensitive = ['send_email', 'make_payment', 'post_message']
if context.tool_name in sensitive: response = context.request_human_input( prompt=f"Approve {context.tool_name}?", default_message="Type 'yes' to approve:" )
if response.lower() != 'yes': return False
return NoneMonitoring and Analytics
Section titled “Monitoring and Analytics”from collections import defaultdictimport time
metrics = defaultdict(lambda: {'count': 0, 'total_time': 0})
@before_tool_calldef start_timer(context): context.tool_input['_start'] = time.time() return None
@after_tool_calldef track_metrics(context): start = context.tool_input.get('_start', time.time()) duration = time.time() - start
metrics[context.tool_name]['count'] += 1 metrics[context.tool_name]['total_time'] += duration
return None
# View metricsdef print_metrics(): for tool, data in metrics.items(): avg = data['total_time'] / data['count'] print(f"{tool}: {data['count']} calls, {avg:.2f}s avg")Response Sanitization
Section titled “Response Sanitization”import re
@after_llm_calldef sanitize_llm_response(context): """Remove sensitive data from LLM responses.""" if not context.response: return None
result = context.response result = re.sub(r'(api[_-]?key)["\']?\s*[:=]\s*["\']?[\w-]+', r'\1: [REDACTED]', result, flags=re.IGNORECASE) return result
@after_tool_calldef sanitize_tool_result(context): """Remove sensitive data from tool results.""" if not context.tool_result: return None
result = context.tool_result result = re.sub(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b', '[EMAIL-REDACTED]', result) return resultHook Management
Section titled “Hook Management”Clearing All Hooks
Section titled “Clearing All Hooks”from crewai.hooks import clear_all_global_hooks
# Clear all hooks at onceresult = clear_all_global_hooks()print(f"Cleared {result['total']} hooks")# Output: {'llm_hooks': (2, 1), 'tool_hooks': (1, 2), 'total': (3, 3)}Clearing Specific Hook Types
Section titled “Clearing Specific Hook Types”from crewai.hooks import ( clear_before_llm_call_hooks, clear_after_llm_call_hooks, clear_before_tool_call_hooks, clear_after_tool_call_hooks)
# Clear specific typesllm_before_count = clear_before_llm_call_hooks()tool_after_count = clear_after_tool_call_hooks()Unregistering Individual Hooks
Section titled “Unregistering Individual Hooks”from crewai.hooks import ( unregister_before_llm_call_hook, unregister_after_tool_call_hook)
def my_hook(context): ...
# Registerregister_before_llm_call_hook(my_hook)
# Later, unregistersuccess = unregister_before_llm_call_hook(my_hook)print(f"Unregistered: {success}")Best Practices
Section titled “Best Practices”1. Keep Hooks Focused
Section titled “1. Keep Hooks Focused”Each hook should have a single, clear responsibility:
# ✅ Good - focused responsibility@before_tool_calldef validate_file_path(context): if context.tool_name == 'read_file': if '..' in context.tool_input.get('path', ''): return False return None
# ❌ Bad - too many responsibilities@before_tool_calldef do_everything(context): # Validation + logging + metrics + approval... ...2. Handle Errors Gracefully
Section titled “2. Handle Errors Gracefully”@before_llm_calldef safe_hook(context): try: # Your logic if some_condition: return False except Exception as e: print(f"Hook error: {e}") return None # Allow execution despite error3. Modify Context In-Place
Section titled “3. Modify Context In-Place”# ✅ Correct - modify in-place@before_llm_calldef add_context(context): context.messages.append({"role": "system", "content": "Be concise"})
# ❌ Wrong - replaces reference@before_llm_calldef wrong_approach(context): context.messages = [{"role": "system", "content": "Be concise"}]4. Use Type Hints
Section titled “4. Use Type Hints”from crewai.hooks import LLMCallHookContext, ToolCallHookContext
def my_llm_hook(context: LLMCallHookContext) -> bool | None: # IDE autocomplete and type checking return None
def my_tool_hook(context: ToolCallHookContext) -> str | None: return None5. Clean Up in Tests
Section titled “5. Clean Up in Tests”import pytestfrom crewai.hooks import clear_all_global_hooks
@pytest.fixture(autouse=True)def clean_hooks(): """Reset hooks before each test.""" yield clear_all_global_hooks()When to Use Which Hook
Section titled “When to Use Which Hook”Use LLM Hooks When:
Section titled “Use LLM Hooks When:”- Implementing iteration limits
- Adding context or safety guidelines to prompts
- Tracking token usage and costs
- Sanitizing or transforming responses
- Implementing approval gates for LLM calls
- Debugging prompt/response interactions
Use Tool Hooks When:
Section titled “Use Tool Hooks When:”- Blocking dangerous or destructive operations
- Validating tool inputs before execution
- Implementing approval gates for sensitive actions
- Caching tool results
- Tracking tool usage and performance
- Sanitizing tool outputs
- Rate limiting tool calls
Use Both When:
Section titled “Use Both When:”Building comprehensive observability, safety, or approval systems that need to monitor all agent operations.
Alternative Registration Methods
Section titled “Alternative Registration Methods”Programmatic Registration (Advanced)
Section titled “Programmatic Registration (Advanced)”For dynamic hook registration or when you need to register hooks programmatically:
from crewai.hooks import ( register_before_llm_call_hook, register_after_tool_call_hook)
def my_hook(context): return None
# Register programmaticallyregister_before_llm_call_hook(my_hook)
# Useful for:# - Loading hooks from configuration# - Conditional hook registration# - Plugin systemsNote: For most use cases, decorators are cleaner and more maintainable.
Performance Considerations
Section titled “Performance Considerations”- Keep Hooks Fast: Hooks execute on every call - avoid heavy computation
- Cache When Possible: Store expensive validations or lookups
- Be Selective: Use crew-scoped hooks when global hooks aren’t needed
- Monitor Hook Overhead: Profile hook execution time in production
- Lazy Import: Import heavy dependencies only when needed
Debugging Hooks
Section titled “Debugging Hooks”Enable Debug Logging
Section titled “Enable Debug Logging”import logging
logging.basicConfig(level=logging.DEBUG)logger = logging.getLogger(__name__)
@before_llm_calldef debug_hook(context): logger.debug(f"LLM call: {context.agent.role}, iteration {context.iterations}") return NoneHook Execution Order
Section titled “Hook Execution Order”Hooks execute in registration order. If a before hook returns False, subsequent hooks don’t execute:
# Register order matters!register_before_tool_call_hook(hook1) # Executes firstregister_before_tool_call_hook(hook2) # Executes secondregister_before_tool_call_hook(hook3) # Executes third
# If hook2 returns False:# - hook1 executed# - hook2 executed and returned False# - hook3 NOT executed# - Tool call blockedRelated Documentation
Section titled “Related Documentation”- LLM Call Hooks → - Detailed LLM hook documentation
- Tool Call Hooks → - Detailed tool hook documentation
- Before and After Kickoff Hooks → - Crew lifecycle hooks
- Human-in-the-Loop → - Human input patterns
Conclusion
Section titled “Conclusion”Execution hooks provide powerful control over agent runtime behavior. Use them to implement safety guardrails, approval workflows, comprehensive monitoring, and custom business logic. Combined with proper error handling, type safety, and performance considerations, hooks enable production-ready, secure, and observable agent systems.