LLM Call Hooks
LLM Call Hooks provide fine-grained control over language model interactions during agent execution. These hooks allow you to intercept LLM calls, modify prompts, transform responses, implement approval gates, and add custom logging or monitoring.
Overview
Section titled “Overview”LLM hooks are executed at two critical points:
- Before LLM Call: Modify messages, validate inputs, or block execution
- After LLM Call: Transform responses, sanitize outputs, or modify conversation history
Hook Types
Section titled “Hook Types”Before LLM Call Hooks
Section titled “Before LLM Call Hooks”Executed before every LLM call, these hooks can:
- Inspect and modify messages sent to the LLM
- Block LLM execution based on conditions
- Implement rate limiting or approval gates
- Add context or system messages
- Log request details
Signature:
def before_hook(context: LLMCallHookContext) -> bool | None: # Return False to block execution # Return True or None to allow execution ...After LLM Call Hooks
Section titled “After LLM Call Hooks”Executed after every LLM call, these hooks can:
- Modify or sanitize LLM responses
- Add metadata or formatting
- Log response details
- Update conversation history
- Implement content filtering
Signature:
def after_hook(context: LLMCallHookContext) -> str | None: # Return modified response string # Return None to keep original response ...LLM Hook Context
Section titled “LLM Hook Context”The LLMCallHookContext object provides comprehensive access to execution state:
class LLMCallHookContext: executor: CrewAgentExecutor # Full executor reference 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 count response: str | None # LLM response (after hooks only)Modifying Messages
Section titled “Modifying Messages”Important: Always modify messages in-place:
# ✅ Correct - modify in-placedef add_context(context: LLMCallHookContext) -> None: context.messages.append({"role": "system", "content": "Be concise"})
# ❌ Wrong - replaces list referencedef wrong_approach(context: LLMCallHookContext) -> None: context.messages = [{"role": "system", "content": "Be concise"}]Registration Methods
Section titled “Registration Methods”1. Global Hook Registration
Section titled “1. Global Hook Registration”Register hooks that apply to all LLM calls across all crews:
from crewai.hooks import register_before_llm_call_hook, register_after_llm_call_hook
def log_llm_call(context): print(f"LLM call by {context.agent.role} at iteration {context.iterations}") return None # Allow execution
register_before_llm_call_hook(log_llm_call)2. Decorator-Based Registration
Section titled “2. Decorator-Based Registration”Use decorators for cleaner syntax:
from crewai.hooks import before_llm_call, after_llm_call
@before_llm_calldef validate_iteration_count(context): if context.iterations > 10: print("⚠️ Exceeded maximum iterations") return False # Block execution return None
@after_llm_calldef sanitize_response(context): if context.response and "API_KEY" in context.response: return context.response.replace("API_KEY", "[REDACTED]") return None3. Crew-Scoped Hooks
Section titled “3. Crew-Scoped Hooks”Register hooks for a specific crew instance:
@CrewBaseclass MyProjCrew: @before_llm_call_crew def validate_inputs(self, context): # Only applies to this crew if context.iterations == 0: print(f"Starting task: {context.task.description}") return None
@after_llm_call_crew def log_responses(self, context): # Crew-specific response logging print(f"Response length: {len(context.response)}") return None
@crew def crew(self) -> Crew: return Crew( agents=self.agents, tasks=self.tasks, process=Process.sequential, verbose=True )Common Use Cases
Section titled “Common Use Cases”1. Iteration Limiting
Section titled “1. Iteration Limiting”@before_llm_calldef limit_iterations(context: LLMCallHookContext) -> bool | None: max_iterations = 15 if context.iterations > max_iterations: print(f"⛔ Blocked: Exceeded {max_iterations} iterations") return False # Block execution return None2. Human Approval Gate
Section titled “2. Human Approval Gate”@before_llm_calldef require_approval(context: LLMCallHookContext) -> bool | None: if context.iterations > 5: response = context.request_human_input( prompt=f"Iteration {context.iterations}: Approve LLM call?", default_message="Press Enter to approve, or type 'no' to block:" ) if response.lower() == "no": print("🚫 LLM call blocked by user") return False return None3. Adding System Context
Section titled “3. Adding System Context”@before_llm_calldef add_guardrails(context: LLMCallHookContext) -> None: # Add safety guidelines to every LLM call context.messages.append({ "role": "system", "content": "Ensure responses are factual and cite sources when possible." }) return None4. Response Sanitization
Section titled “4. Response Sanitization”@after_llm_calldef sanitize_sensitive_data(context: LLMCallHookContext) -> str | None: if not context.response: return None
# Remove sensitive patterns import re sanitized = context.response sanitized = re.sub(r'\b\d{3}-\d{2}-\d{4}\b', '[SSN-REDACTED]', sanitized) sanitized = re.sub(r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b', '[CARD-REDACTED]', sanitized)
return sanitized5. Cost Tracking
Section titled “5. Cost Tracking”import tiktoken
@before_llm_calldef track_token_usage(context: LLMCallHookContext) -> None: encoding = tiktoken.get_encoding("cl100k_base") total_tokens = sum( len(encoding.encode(msg.get("content", ""))) for msg in context.messages ) print(f"📊 Input tokens: ~{total_tokens}") return None
@after_llm_calldef track_response_tokens(context: LLMCallHookContext) -> None: if context.response: encoding = tiktoken.get_encoding("cl100k_base") tokens = len(encoding.encode(context.response)) print(f"📊 Response tokens: ~{tokens}") return None6. Debug Logging
Section titled “6. Debug Logging”@before_llm_calldef debug_request(context: LLMCallHookContext) -> None: print(f""" 🔍 LLM Call Debug: - Agent: {context.agent.role} - Task: {context.task.description[:50]}... - Iteration: {context.iterations} - Message Count: {len(context.messages)} - Last Message: {context.messages[-1] if context.messages else 'None'} """) return None
@after_llm_calldef debug_response(context: LLMCallHookContext) -> None: if context.response: print(f"✅ Response Preview: {context.response[:100]}...") return NoneHook Management
Section titled “Hook Management”Unregistering Hooks
Section titled “Unregistering Hooks”from crewai.hooks import ( unregister_before_llm_call_hook, unregister_after_llm_call_hook)
# Unregister specific hookdef my_hook(context): ...
register_before_llm_call_hook(my_hook)# Later...unregister_before_llm_call_hook(my_hook) # Returns True if foundClearing Hooks
Section titled “Clearing Hooks”from crewai.hooks import ( clear_before_llm_call_hooks, clear_after_llm_call_hooks, clear_all_llm_call_hooks)
# Clear specific hook typecount = clear_before_llm_call_hooks()print(f"Cleared {count} before hooks")
# Clear all LLM hooksbefore_count, after_count = clear_all_llm_call_hooks()print(f"Cleared {before_count} before and {after_count} after hooks")Listing Registered Hooks
Section titled “Listing Registered Hooks”from crewai.hooks import ( get_before_llm_call_hooks, get_after_llm_call_hooks)
# Get current hooksbefore_hooks = get_before_llm_call_hooks()after_hooks = get_after_llm_call_hooks()
print(f"Registered: {len(before_hooks)} before, {len(after_hooks)} after")Advanced Patterns
Section titled “Advanced Patterns”Conditional Hook Execution
Section titled “Conditional Hook Execution”@before_llm_calldef conditional_blocking(context: LLMCallHookContext) -> bool | None: # Only block for specific agents if context.agent.role == "researcher" and context.iterations > 10: return False
# Only block for specific tasks if "sensitive" in context.task.description.lower() and context.iterations > 5: return False
return NoneContext-Aware Modifications
Section titled “Context-Aware Modifications”@before_llm_calldef adaptive_prompting(context: LLMCallHookContext) -> None: # Add different context based on iteration if context.iterations == 0: context.messages.append({ "role": "system", "content": "Start with a high-level overview." }) elif context.iterations > 3: context.messages.append({ "role": "system", "content": "Focus on specific details and provide examples." }) return NoneChaining Hooks
Section titled “Chaining Hooks”# Multiple hooks execute in registration order
@before_llm_calldef first_hook(context): print("1. First hook executed") return None
@before_llm_calldef second_hook(context): print("2. Second hook executed") return None
@before_llm_calldef blocking_hook(context): if context.iterations > 10: print("3. Blocking hook - execution stopped") return False # Subsequent hooks won't execute print("3. Blocking hook - execution allowed") return NoneBest Practices
Section titled “Best Practices”- Keep Hooks Focused: Each hook should have a single responsibility
- Avoid Heavy Computation: Hooks execute on every LLM call
- Handle Errors Gracefully: Use try-except to prevent hook failures from breaking execution
- Use Type Hints: Leverage
LLMCallHookContextfor better IDE support - Document Hook Behavior: Especially for blocking conditions
- Test Hooks Independently: Unit test hooks before using in production
- Clear Hooks in Tests: Use
clear_all_llm_call_hooks()between test runs - Modify In-Place: Always modify
context.messagesin-place, never replace
Error Handling
Section titled “Error Handling”@before_llm_calldef safe_hook(context: LLMCallHookContext) -> bool | None: try: # Your hook logic if some_condition: return False except Exception as e: print(f"⚠️ Hook error: {e}") # Decide: allow or block on error return None # Allow execution despite errorType Safety
Section titled “Type Safety”from crewai.hooks import LLMCallHookContext, BeforeLLMCallHookType, AfterLLMCallHookType
# Explicit type annotationsdef my_before_hook(context: LLMCallHookContext) -> bool | None: return None
def my_after_hook(context: LLMCallHookContext) -> str | None: return None
# Type-safe registrationregister_before_llm_call_hook(my_before_hook)register_after_llm_call_hook(my_after_hook)Troubleshooting
Section titled “Troubleshooting”Hook Not Executing
Section titled “Hook Not Executing”- Verify hook is registered before crew execution
- Check if previous hook returned
False(blocks subsequent hooks) - Ensure hook signature matches expected type
Message Modifications Not Persisting
Section titled “Message Modifications Not Persisting”- Use in-place modifications:
context.messages.append() - Don’t replace the list:
context.messages = []
Response Modifications Not Working
Section titled “Response Modifications Not Working”- Return the modified string from after hooks
- Returning
Nonekeeps the original response
Conclusion
Section titled “Conclusion”LLM Call Hooks provide powerful capabilities for controlling and monitoring language model interactions in CrewAI. Use them to implement safety guardrails, approval gates, logging, cost tracking, and response sanitization. Combined with proper error handling and type safety, hooks enable robust and production-ready agent systems.