Akshay Parkhi's Weblog

Subscribe

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 EventCalled In
AgentInitializedEventagent.py — end of __init__
BeforeInvocationEventagent.py — start of __call__
AfterInvocationEventagent.py — end of __call__ / structured_output
BeforeModelCallEventevent_loop.py — before stream_messages()
AfterModelCallEventevent_loop.py — after model response completes
MessageAddedEventevent_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 write cancel_tool, selected_tool, tool_use
  • AfterToolCallEvent: can write result, retry
  • AfterModelCallEvent: can write retry
  • Most other fields raise AttributeError if 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

  1. 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.
  2. 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.
  3. Don’t modify agent.messages directly in hooks — Use the writable event properties instead. Directly mutating messages can break the conversation flow.
  4. Don’t use hooks for tool implementation — If you need a tool, use @tool. Hooks that set cancel_tool and inject fake results are a code smell if they’re replacing what should be a proper tool.
  5. Don’t use AgentInitializedEvent with async callbacks — It raises ValueError. The agent isn’t in an async context during __init__.
  6. 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.
  7. Don’t use hooks to implement conversation management — Use ConversationManager instead, which already integrates as a hook internally.