How AWS Strands Hooks Work
7th March 2026
What Hooks Are
Hooks are an event-driven extensibility system — a way to inject custom logic at specific points in the agent lifecycle without modifying core code. Think of them as middleware/interceptors.
The Hook Events (Where They Fire)
Here’s every hook point in the lifecycle, in execution order:
Agent.__init__()
└─ AgentInitializedEvent ← Agent fully constructed
Agent("do something")
└─ BeforeInvocationEvent ← Before any processing starts
│
└─ [Event Loop Cycle]
│
├─ BeforeModelCallEvent ← Before calling the LLM
│ └─ (LLM streaming happens)
├─ AfterModelCallEvent ← After LLM responds (or errors)
│ └─ can set retry=True to re-call model
│
├─ MessageAddedEvent ← Assistant message appended to history
│
├─ [For each tool call]:
│ ├─ BeforeToolCallEvent ← Before tool executes
│ │ └─ can cancel_tool, swap selected_tool, modify tool_use
│ ├─ (tool runs)
│ ├─ AfterToolCallEvent ← After tool completes
│ │ └─ can set retry=True, modify result
│ └─ MessageAddedEvent ← Tool result appended to history
│
└─ [Recurse if LLM wants more tools]
│
└─ AfterInvocationEvent ← After everything completes (always fires, even on error)
Where Hooks Are Called (Code Locations)
| Hook Event | Called In |
|---|---|
AgentInitializedEvent | agent.py — end of __init__ |
BeforeInvocationEvent | agent.py — start of __call__ |
AfterInvocationEvent | agent.py — end of __call__ / structured_output |
BeforeModelCallEvent | event_loop.py — before stream_messages() |
AfterModelCallEvent | event_loop.py — after model response completes |
MessageAddedEvent | event_loop.py — after message appended to history |
BeforeToolCallEvent | _executor.py — before tool execution |
AfterToolCallEvent | _executor.py — after tool completes |
How Registration Works
# Method 1: Direct callback registration
agent = Agent()
agent.hooks.add_callback(BeforeToolCallEvent, my_callback_fn)
# Method 2: HookProvider class (preferred for grouped hooks)
class MyHooks(HookProvider):
def register_hooks(self, registry: HookRegistry):
registry.add_callback(BeforeToolCallEvent, self.on_before_tool)
registry.add_callback(AfterToolCallEvent, self.on_after_tool)
agent = Agent(hooks=[MyHooks()])
# Method 3: Built-in components auto-register
# session_manager, conversation_manager, retry_strategy all implement HookProvider
Built-in components that register as hooks (in agent.py):
- SessionManager — persists conversation state
- ConversationManager — handles context window overflow
- RetryStrategy — retries on model errors (uses
AfterModelCallEvent.retry)
What Hooks Are Used For
1. Human-in-the-loop (Interrupts)
def approve_tool(event: BeforeToolCallEvent):
if not user_approves(event.tool_use):
event.cancel_tool = "User rejected" # Cancel the tool
# Or raise InterruptException for async approval workflows
2. Tool swapping/routing
def route_tool(event: BeforeToolCallEvent):
event.selected_tool = my_alternative_tool # Swap which tool runs
3. Retry logic
def retry_on_failure(event: AfterModelCallEvent):
if event.exception and isinstance(event.exception, ThrottledException):
time.sleep(backoff)
event.retry = True # Re-call the model
4. Logging/observability
def log_all(event: BeforeModelCallEvent | AfterModelCallEvent):
logger.info("Model event: %s", type(event).__name__)
5. Message redaction/transformation
def redact(event: BeforeInvocationEvent):
event.messages = sanitize(event.messages) # Modify input before processing
6. Steering (experimental)
Dynamically inject guidance before tool calls.
Key Design Details
Paired events with reverse ordering: After* events fire callbacks in reverse registration order. If hooks A, B, C registered in that order, BeforeX fires A→B→C but AfterX fires C→B→A. This ensures proper cleanup (like stack unwinding).
Writable vs read-only fields: Events use _can_write() to control which fields hooks can modify. For example:
BeforeToolCallEvent: can writecancel_tool,selected_tool,tool_useAfterToolCallEvent: can writeresult,retryAfterModelCallEvent: can writeretry- Most other fields raise
AttributeErrorif you try to set them
Async support: All hook invocations use invoke_callbacks_async (except AgentInitializedEvent which must be sync since it fires in __init__).
Where You Should NOT Use Hooks
- Don’t use hooks for core business logic — Hooks are for cross-cutting concerns (logging, auth, retry). If your logic IS the agent’s purpose, put it in tools or the system prompt.
- Don’t do heavy computation in hooks — They run inline in the event loop. A slow hook blocks the entire agent cycle. Especially avoid blocking I/O in sync callbacks.
- Don’t modify agent.messages directly in hooks — Use the writable event properties instead. Directly mutating messages can break the conversation flow.
- Don’t use hooks for tool implementation — If you need a tool, use
@tool. Hooks that setcancel_tooland inject fake results are a code smell if they’re replacing what should be a proper tool. - Don’t use AgentInitializedEvent with async callbacks — It raises
ValueError. The agent isn’t in an async context during__init__. - Don’t depend on hook execution order across different providers — Multiple HookProviders register in the order they’re passed, but this coupling is fragile. Design hooks to be independent.
- Don’t use hooks to implement conversation management — Use
ConversationManagerinstead, which already integrates as a hook internally.
More recent articles
- OpenUSD: Advanced Patterns and Common Gotchas. - 28th March 2026
- OpenUSD Mastery: From Composition to Pipeline — A SO-101 Arm Journey - 25th March 2026
- Learning OpenUSD — From Curious Questions to Real Understanding - 19th March 2026