diff --git a/src/config/navigation.yml b/src/config/navigation.yml
index 5443656c1..50a1bbaf1 100644
--- a/src/config/navigation.yml
+++ b/src/config/navigation.yml
@@ -56,7 +56,11 @@ sidebar:
- docs/user-guide/concepts/agents/session-management
- docs/user-guide/concepts/agents/prompts
- docs/user-guide/concepts/agents/retry-strategies
- - docs/user-guide/concepts/agents/hooks
+ - label: Hooks
+ items:
+ - docs/user-guide/concepts/agents/hooks
+ - docs/user-guide/concepts/agents/hooks/usage
+ - docs/user-guide/concepts/agents/hooks/cookbook
- docs/user-guide/concepts/agents/structured-output
- docs/user-guide/concepts/agents/conversation-management
- label: Tools
diff --git a/src/content/docs/user-guide/concepts/agents/hooks.mdx b/src/content/docs/user-guide/concepts/agents/hooks.mdx
deleted file mode 100644
index 064ab1eca..000000000
--- a/src/content/docs/user-guide/concepts/agents/hooks.mdx
+++ /dev/null
@@ -1,1207 +0,0 @@
----
-title: Hooks
-description: "Intercept and customize agent behavior at every step. Hooks let you add logging, validation, guardrails, and custom logic to the agent loop."
----
-
-
-Hooks are a composable extensibility mechanism for extending agent functionality by subscribing to events throughout the agent lifecycle. The hook system enables both built-in components and user code to react to or modify agent behavior through strongly-typed event callbacks.
-
-## Overview
-
-The hooks system is a composable, type-safe system that supports multiple subscribers per event type.
-
-A **Hook Event** is a specific event in the lifecycle that callbacks can be associated with. A **Hook Callback** is a callback function that is invoked when the hook event is emitted.
-
-Hooks enable use cases such as:
-
-- Monitoring agent execution and tool usage
-- Modifying tool execution behavior
-- Adding validation and error handling
-- Monitoring multi-agent execution flow and node transitions
-- Debugging complex orchestration patterns
-- Implementing custom logging and metrics collection
-
-## Basic Usage
-
-Hook callbacks are registered against specific event types and receive strongly-typed event objects when those events occur during agent execution. Each event carries relevant data for that stage of the agent lifecycle - for example, `BeforeInvocationEvent` includes agent and request details, while `BeforeToolCallEvent` provides tool information and parameters.
-
-### Registering Individual Hook Callbacks
-
-The simplest way to register a hook callback is using the `agent.add_hook()` method:
-
-
-
-
-```python
-from strands import Agent
-from strands.hooks import BeforeInvocationEvent, BeforeToolCallEvent
-
-agent = Agent()
-
-# Register individual callbacks
-def my_callback(event: BeforeInvocationEvent) -> None:
- print("Custom callback triggered")
-
-agent.add_hook(my_callback, BeforeInvocationEvent)
-
-# Type inference: If your callback has a type hint, the event type is inferred
-def typed_callback(event: BeforeToolCallEvent) -> None:
- print(f"Tool called: {event.tool_use['name']}")
-
-agent.add_hook(typed_callback) # Event type inferred from type hint
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/agents/hooks.ts:individual_callback"
-```
-
-
-
-For multi-agent orchestrators, you can register callbacks for orchestration events:
-
-
-
-
-```python
-# Create your orchestrator (Graph or Swarm)
-orchestrator = Graph(...)
-
-# Register individual callbacks
-def my_callback(event: BeforeNodeCallEvent) -> None:
- print(f"Custom callback triggered")
-
-orchestrator.hooks.add_callback(BeforeNodeCallEvent, my_callback)
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/agents/hooks.ts:orchestrator_callback"
-```
-
-
-
-### Using Plugins for Multiple Hooks
-
-For packaging multiple related hooks together, [Plugins](../plugins/index.md) provide a convenient way to bundle hooks with configuration and tools:
-
-
-
-
-```python
-from strands import Agent
-from strands.plugins import Plugin, hook
-from strands.hooks import BeforeToolCallEvent, AfterToolCallEvent
-
-class LoggingPlugin(Plugin):
- name = "logging-plugin"
-
- @hook
- def log_before(self, event: BeforeToolCallEvent) -> None:
- print(f"Calling: {event.tool_use['name']}")
-
- @hook
- def log_after(self, event: AfterToolCallEvent) -> None:
- print(f"Completed: {event.tool_use['name']}")
-
-agent = Agent(plugins=[LoggingPlugin()])
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/plugins/index.ts:plugin_for_hooks"
-```
-
-
-
-See [Plugins](../plugins/index.md) for more information on creating and using plugins.
-
-## Hook Event Lifecycle
-
-### Single-Agent Lifecycle
-
-The following diagram shows when hook events are emitted during a typical agent invocation where tools are invoked:
-
-
-
-
-```mermaid
-flowchart LR
- subgraph Start["Request Start Events"]
- direction TB
- BeforeInvocationEvent["BeforeInvocationEvent"]
- StartMessage["MessageAddedEvent"]
- BeforeInvocationEvent --> StartMessage
- end
- subgraph Model["Model Events"]
- direction TB
- BeforeModelCallEvent["BeforeModelCallEvent"]
- AfterModelCallEvent["AfterModelCallEvent"]
- ModelMessage["MessageAddedEvent"]
- BeforeModelCallEvent --> AfterModelCallEvent
- AfterModelCallEvent --> ModelMessage
- end
- subgraph Tool["Tool Events"]
- direction TB
- BeforeToolCallEvent["BeforeToolCallEvent"]
- AfterToolCallEvent["AfterToolCallEvent"]
- ToolMessage["MessageAddedEvent"]
- BeforeToolCallEvent --> AfterToolCallEvent
- AfterToolCallEvent --> ToolMessage
- end
- subgraph End["Request End Events"]
- direction TB
- AfterInvocationEvent["AfterInvocationEvent"]
- end
-Start --> Model
-Model <--> Tool
-Tool --> End
-```
-
-
-
-```mermaid
-flowchart LR
- subgraph Start["Request Start Events"]
- direction TB
- BeforeInvocationEvent["BeforeInvocationEvent"]
- StartMessage["MessageAddedEvent"]
- BeforeInvocationEvent --> StartMessage
- end
- subgraph Model["Model Events"]
- direction TB
- BeforeModelCallEvent["BeforeModelCallEvent"]
- ModelStreamUpdateEvent["ModelStreamUpdateEvent"]
- ContentBlockEvent["ContentBlockEvent"]
- ModelMessageEvent["ModelMessageEvent"]
- AfterModelCallEvent["AfterModelCallEvent"]
- ModelMessage["MessageAddedEvent"]
- BeforeModelCallEvent --> ModelStreamUpdateEvent
- ModelStreamUpdateEvent --> ContentBlockEvent
- ContentBlockEvent --> ModelMessageEvent
- ModelMessageEvent --> AfterModelCallEvent
- AfterModelCallEvent --> ModelMessage
- end
- subgraph Tool["Tool Events"]
- direction TB
- BeforeToolCallEvent["BeforeToolCallEvent"]
- ToolStreamUpdateEvent["ToolStreamUpdateEvent"]
- ToolResultEvent["ToolResultEvent"]
- AfterToolCallEvent["AfterToolCallEvent"]
- ToolMessage["MessageAddedEvent"]
- BeforeToolCallEvent --> ToolStreamUpdateEvent
- ToolStreamUpdateEvent --> ToolResultEvent
- ToolResultEvent --> AfterToolCallEvent
- AfterToolCallEvent --> ToolMessage
- end
- subgraph End["Request End Events"]
- direction TB
- AgentResultEvent["AgentResultEvent"]
- AfterInvocationEvent["AfterInvocationEvent"]
- AgentResultEvent --> AfterInvocationEvent
- end
-Start --> Model
-Model <--> Tool
-Tool --> End
-```
-
-
-
-### Multi-Agent Lifecycle
-
-The following diagram shows when multi-agent hook events are emitted during orchestrator execution:
-
-
-
-
-```mermaid
-flowchart LR
-subgraph Init["Initialization"]
- direction TB
- MultiAgentInitializedEvent["MultiAgentInitializedEvent"]
-end
-subgraph Invocation["Invocation Lifecycle"]
- direction TB
- BeforeMultiAgentInvocationEvent["BeforeMultiAgentInvocationEvent"]
- AfterMultiAgentInvocationEvent["AfterMultiAgentInvocationEvent"]
- BeforeMultiAgentInvocationEvent --> NodeExecution
- NodeExecution --> AfterMultiAgentInvocationEvent
-end
-subgraph NodeExecution["Node Execution (Repeated)"]
- direction TB
- BeforeNodeCallEvent["BeforeNodeCallEvent"]
- AfterNodeCallEvent["AfterNodeCallEvent"]
- BeforeNodeCallEvent --> AfterNodeCallEvent
-end
-Init --> Invocation
-```
-
-
-
-```mermaid
-flowchart LR
-subgraph Init["Initialization"]
- direction TB
- MultiAgentInitializedEvent["MultiAgentInitializedEvent"]
-end
-subgraph Invocation["Invocation Lifecycle"]
- direction TB
- BeforeMultiAgentInvocationEvent["BeforeMultiAgentInvocationEvent"]
- AfterMultiAgentInvocationEvent["AfterMultiAgentInvocationEvent"]
- MultiAgentResultEvent["MultiAgentResultEvent"]
- BeforeMultiAgentInvocationEvent --> NodeExecution
- NodeExecution --> AfterMultiAgentInvocationEvent
- AfterMultiAgentInvocationEvent --> MultiAgentResultEvent
-end
-subgraph NodeExecution["Node Execution (Repeated)"]
- direction TB
- BeforeNodeCallEvent["BeforeNodeCallEvent"]
- NodeStreamUpdateEvent["NodeStreamUpdateEvent"]
- AfterNodeCallEvent["AfterNodeCallEvent"]
- NodeResultEvent["NodeResultEvent"]
- MultiAgentHandoffEvent["MultiAgentHandoffEvent"]
- BeforeNodeCallEvent --> NodeStreamUpdateEvent
- NodeStreamUpdateEvent --> AfterNodeCallEvent
- AfterNodeCallEvent --> NodeResultEvent
- NodeResultEvent --> MultiAgentHandoffEvent
-end
-Init --> Invocation
-```
-
-
-
-### Available Events
-
-
-
-
-| Event | Description |
-|------------------------------------|---------------------------------------------------------------------------------------------------------------|
-| `AgentInitializedEvent` | Triggered when an agent has been constructed and finished initialization at the end of the agent constructor. |
-| `BeforeInvocationEvent` | Triggered at the beginning of a new agent invocation request |
-| `AfterInvocationEvent` | Triggered at the end of an agent request, regardless of success or failure. Uses reverse callback ordering |
-| `MessageAddedEvent` | Triggered when a message is added to the agent's conversation history |
-| `BeforeModelCallEvent` | Triggered before the model is invoked for inference |
-| `AfterModelCallEvent` | Triggered after model invocation completes. Uses reverse callback ordering |
-| `BeforeToolCallEvent` | Triggered before a tool is invoked |
-| `AfterToolCallEvent` | Triggered after tool invocation completes. Uses reverse callback ordering |
-| `MultiAgentInitializedEvent` | Triggered when multi-agent orchestrator is initialized |
-| `BeforeMultiAgentInvocationEvent` | Triggered before orchestrator execution starts |
-| `AfterMultiAgentInvocationEvent` | Triggered after orchestrator execution completes. Uses reverse callback ordering |
-| `BeforeNodeCallEvent` | Triggered before individual node execution starts |
-| `AfterNodeCallEvent` | Triggered after individual node execution completes. Uses reverse callback ordering |
-
-
-
-All events extend `HookableEvent`, making them both streamable via `agent.stream()` and subscribable via hook callbacks.
-
-| Event | Description |
-|--------------------------|---------------------------------------------------------------------------------------------------------------|
-| `AgentInitializedEvent` | Triggered when an agent has been constructed and finished initialization at the end of the agent constructor. |
-| `BeforeInvocationEvent` | Triggered at the beginning of a new agent invocation request |
-| `AfterInvocationEvent` | Triggered at the end of an agent request, regardless of success or failure. Uses reverse callback ordering |
-| `MessageAddedEvent` | Triggered when a message is added to the agent's conversation history |
-| `BeforeModelCallEvent` | Triggered before the model is invoked for inference |
-| `AfterModelCallEvent` | Triggered after model invocation completes. Uses reverse callback ordering |
-| `ModelStreamUpdateEvent` | Wraps each transient streaming delta from the model during inference. Access via `.event` |
-| `ContentBlockEvent` | Wraps a fully assembled content block (TextBlock, ToolUseBlock, ReasoningBlock). Access via `.contentBlock` |
-| `ModelMessageEvent` | Wraps the complete model message after all blocks are assembled. Access via `.message` |
-| `BeforeToolCallEvent` | Triggered before a tool is invoked |
-| `AfterToolCallEvent` | Triggered after tool invocation completes. Uses reverse callback ordering |
-| `BeforeToolsEvent` | Triggered before tools are executed in a batch |
-| `AfterToolsEvent` | Triggered after tools are executed in a batch. Uses reverse callback ordering |
-| `ToolStreamUpdateEvent` | Wraps streaming progress events from tool execution. Access via `.event` |
-| `ToolResultEvent` | Wraps a completed tool result. Access via `.result` |
-| `AgentResultEvent` | Wraps the final agent result at the end of the invocation. Access via `.result` |
-| `MultiAgentInitializedEvent` | Triggered when a multi-agent orchestrator has finished initialization |
-| `BeforeMultiAgentInvocationEvent` | Triggered before orchestrator execution starts |
-| `AfterMultiAgentInvocationEvent` | Triggered after orchestrator execution completes. Uses reverse callback ordering |
-| `BeforeNodeCallEvent` | Triggered before individual node execution starts |
-| `NodeStreamUpdateEvent` | Wraps an inner streaming event from a node with the node's identity. Access via `.event` |
-| `NodeCancelEvent` | Triggered when a node is cancelled via `BeforeNodeCallEvent.cancel` |
-| `AfterNodeCallEvent` | Triggered after individual node execution completes. Uses reverse callback ordering |
-| `NodeResultEvent` | Wraps a completed node result. Access via `.result` |
-| `MultiAgentHandoffEvent` | Triggered when execution transitions between nodes |
-| `MultiAgentResultEvent` | Wraps the final multi-agent result at the end of orchestration. Access via `.result` |
-
-
-
-## Hook Behaviors
-
-### Event Properties
-
-Most event properties are read-only to prevent unintended modifications. However, certain properties can be modified to influence agent behavior:
-
-
-
-
-- [`AfterModelCallEvent`](@api/python/strands.hooks.events#AfterModelCallEvent)
- - `retry` - Request a retry of the model invocation. See [Model Call Retry](#model-call-retry).
-
-- [`BeforeToolCallEvent`](@api/python/strands.hooks.events#BeforeToolCallEvent)
- - `cancel_tool` - Cancel tool execution with a message. See [Limit Tool Counts](#limit-tool-counts).
- - `selected_tool` - Replace the tool to be executed. See [Tool Interception](#tool-interception).
- - `tool_use` - Modify tool parameters before execution. See [Fixed Tool Arguments](#fixed-tool-arguments).
-
-- [`AfterToolCallEvent`](@api/python/strands.hooks.events#AfterToolCallEvent)
- - `result` - Modify the tool result. See [Result Modification](#result-modification).
- - `retry` - Request a retry of the tool invocation. See [Tool Call Retry](#tool-call-retry).
- - `exception` *(read-only)* - The original exception if the tool raised one, otherwise `None`. See [Exception Handling](#exception-handling).
-
-- [`AfterInvocationEvent`](@api/python/strands.hooks.events#AfterInvocationEvent)
- - `resume` - Trigger a follow-up agent invocation with new input. See [Invocation resume](#invocation-resume).
-
-
-
-
-- `BeforeToolsEvent`
- - `cancel` - Cancel all tool calls in a batch with a message. See [Limit Tool Counts](#limit-tool-counts).
-
-- `BeforeToolCallEvent`
- - `cancel` - Cancel tool execution with a message. See [Limit Tool Counts](#limit-tool-counts).
-
-- `AfterModelCallEvent`
- - `retry` - Request a retry of the model invocation.
-
-- `AfterToolCallEvent`
- - `retry` - Request a retry of the tool invocation.
-
-
-
-### Callback Ordering
-
-Some events come in pairs, such as Before/After events. The After event callbacks are always called in reverse order from the Before event callbacks to ensure proper cleanup semantics.
-
-## Advanced Usage
-
-### Accessing Invocation State in Hooks
-
-Invocation state provides configuration and context data passed through the agent or orchestrator invocation. This is particularly useful for:
-
-1. **Custom Objects**: Access database client objects, connection pools, or other Python objects
-2. **Request Context**: Access session IDs, user information, settings, or request-specific data
-3. **Multi-Agent Shared State**: In multi-agent patterns, access state shared across all agents - see [Shared State Across Multi-Agent Patterns](../multi-agent/multi-agent-patterns.md#shared-state-across-multi-agent-patterns)
-4. **Custom Parameters**: Pass any additional data that hooks might need
-
-
-
-
-```python
-from strands.hooks import BeforeToolCallEvent
-import logging
-
-def log_with_context(event: BeforeToolCallEvent) -> None:
- """Log tool invocations with context from invocation state."""
- # Access invocation state from the event
- user_id = event.invocation_state.get("user_id", "unknown")
- session_id = event.invocation_state.get("session_id")
-
- # Access non-JSON serializable objects like database connections
- db_connection = event.invocation_state.get("database_connection")
- logger_instance = event.invocation_state.get("custom_logger")
-
- # Use custom logger if provided, otherwise use default
- logger = logger_instance if logger_instance else logging.getLogger(__name__)
-
- logger.info(
- f"User {user_id} in session {session_id} "
- f"invoking tool: {event.tool_use['name']} "
- f"with DB connection: {db_connection is not None}"
- )
-
-# Register the hook
-agent = Agent(tools=[my_tool])
-agent.hooks.add_callback(BeforeToolCallEvent, log_with_context)
-
-# Execute with context including non-serializable objects
-import sqlite3
-custom_logger = logging.getLogger("custom")
-db_conn = sqlite3.connect(":memory:")
-
-result = agent(
- "Process the data",
- user_id="user123",
- session_id="sess456",
- database_connection=db_conn, # Non-JSON serializable object
- custom_logger=custom_logger # Non-JSON serializable object
-)
-```
-
-
-
-```ts
-// This feature is not yet available in TypeScript SDK
-```
-
-
-
-Multi-agent hook events provide access to:
-
-
-
-
-- **source**: The multi-agent orchestrator instance (for example: Graph/Swarm)
-- **node_id**: Identifier of the node being executed (for node-level events)
-- **invocation_state**: Configuration and context data passed through the orchestrator invocation
-
-
-
-- **orchestrator**: The multi-agent orchestrator instance (for example: Graph/Swarm)
-- **nodeId**: Identifier of the node being executed (for node-level events)
-- **state**: The `MultiAgentState` for the current invocation, including an `app` field for custom consumer state
-
-
-
-### Tool Interception
-
-Modify or replace tools before execution:
-
-
-
-
-```python
-class ToolInterceptor(HookProvider):
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(BeforeToolCallEvent, self.intercept_tool)
-
- def intercept_tool(self, event: BeforeToolCallEvent) -> None:
- if event.tool_use.name == "sensitive_tool":
- # Replace with a safer alternative
- event.selected_tool = self.safe_alternative_tool
- event.tool_use["name"] = "safe_tool"
-```
-
-
-
-```ts
-// Changing of tools is not yet available in TypeScript SDK
-```
-
-
-
-### Result Modification
-
-Modify tool results after execution:
-
-
-
-
-```python
-class ResultProcessor(HookProvider):
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(AfterToolCallEvent, self.process_result)
-
- def process_result(self, event: AfterToolCallEvent) -> None:
- if event.tool_use.name == "calculator":
- # Add formatting to calculator results
- original_content = event.result["content"][0]["text"]
- event.result["content"][0]["text"] = f"Result: {original_content}"
-```
-
-
-
-```ts
-// Changing of tool results is not yet available in TypeScript SDK
-```
-
-
-
-### Conditional Node Execution
-
-Implement custom logic to modify orchestration behavior in multi-agent systems:
-
-
-
-
-```python
-class ConditionalExecutionHook(HookProvider):
- def __init__(self, skip_conditions: dict[str, callable]):
- self.skip_conditions = skip_conditions
-
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(BeforeNodeCallEvent, self.check_execution_conditions)
-
- def check_execution_conditions(self, event: BeforeNodeCallEvent) -> None:
- node_id = event.node_id
- if node_id in self.skip_conditions:
- condition_func = self.skip_conditions[node_id]
- if condition_func(event.invocation_state):
- print(f"Skipping node {node_id} due to condition")
- # Note: Actual node skipping would require orchestrator-specific implementation
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/agents/hooks.ts:conditional_node_execution"
-```
-
-
-
-## Best Practices
-
-### Composability
-
-Design hooks to be composable and reusable:
-
-
-
-
-```python
-class RequestLoggingHook(HookProvider):
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(BeforeInvocationEvent, self.log_request)
- registry.add_callback(AfterInvocationEvent, self.log_response)
- registry.add_callback(BeforeToolCallEvent, self.log_tool_use)
-
- ...
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/agents/hooks.ts:composability"
-```
-
-
-
-### Event Property Modifications
-
-When modifying event properties, log the changes for debugging and audit purposes:
-
-
-
-
-```python
-class ResultProcessor(HookProvider):
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(AfterToolCallEvent, self.process_result)
-
- def process_result(self, event: AfterToolCallEvent) -> None:
- if event.tool_use.name == "calculator":
- original_content = event.result["content"][0]["text"]
- logger.info(f"Modifying calculator result: {original_content}")
- event.result["content"][0]["text"] = f"Result: {original_content}"
-```
-
-
-
-```ts
-// Changing of tools is not yet available in TypeScript SDK
-```
-
-
-
-### Orchestrator-Agnostic Design
-
-Design multi-agent hooks to work with different orchestrator types:
-
-
-
-
-```python
-class UniversalMultiAgentHook(HookProvider):
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(BeforeNodeCallEvent, self.handle_node_execution)
-
- def handle_node_execution(self, event: BeforeNodeCallEvent) -> None:
- orchestrator_type = type(event.source).__name__
- print(f"Executing node {event.node_id} in {orchestrator_type} orchestrator")
-
- # Handle orchestrator-specific logic if needed
- if orchestrator_type == "Graph":
- self.handle_graph_node(event)
- elif orchestrator_type == "Swarm":
- self.handle_swarm_node(event)
-
- def handle_graph_node(self, event: BeforeNodeCallEvent) -> None:
- # Graph-specific handling
- pass
-
- def handle_swarm_node(self, event: BeforeNodeCallEvent) -> None:
- # Swarm-specific handling
- pass
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/agents/hooks.ts:orchestrator_agnostic_design"
-```
-
-
-
-## Integration with Multi-Agent Systems
-
-Multi-agent hooks complement single-agent hooks. Individual agents within the orchestrator can still have their own hooks, creating a layered monitoring and customization system:
-
-
-
-
-```python
-# Single-agent hook for individual agents
-class AgentLevelHook(HookProvider):
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(BeforeToolCallEvent, self.log_tool_use)
-
- def log_tool_use(self, event: BeforeToolCallEvent) -> None:
- print(f"Agent tool call: {event.tool_use['name']}")
-
-# Multi-agent hook for orchestrator
-class OrchestratorLevelHook(HookProvider):
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(BeforeNodeCallEvent, self.log_node_execution)
-
- def log_node_execution(self, event: BeforeNodeCallEvent) -> None:
- print(f"Orchestrator node execution: {event.node_id}")
-
-# Create agents with individual hooks
-agent1 = Agent(tools=[tool1], hooks=[AgentLevelHook()])
-agent2 = Agent(tools=[tool2], hooks=[AgentLevelHook()])
-
-# Create orchestrator with multi-agent hooks
-orchestrator = Graph(
- agents={"agent1": agent1, "agent2": agent2},
- hooks=[OrchestratorLevelHook()]
-)
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/agents/hooks.ts:layered_hooks"
-```
-
-
-
-This layered approach provides comprehensive observability and control across both individual agent execution and orchestrator-level coordination.
-
-## Cookbook
-
-This section contains practical hook implementations for common use cases.
-
-### Fixed Tool Arguments
-
-Useful for enforcing security policies, maintaining consistency, or overriding agent decisions with system-level requirements. This hook ensures specific tools always use predetermined parameter values regardless of what the agent specifies.
-
-
-
-
-```python
-from typing import Any
-from strands.hooks import HookProvider, HookRegistry, BeforeToolCallEvent
-
-class ConstantToolArguments(HookProvider):
- """Use constant argument values for specific parameters of a tool."""
-
- def __init__(self, fixed_tool_arguments: dict[str, dict[str, Any]]):
- """
- Initialize fixed parameter values for tools.
-
- Args:
- fixed_tool_arguments: A dictionary mapping tool names to dictionaries of
- parameter names and their fixed values. These values will override any
- values provided by the agent when the tool is invoked.
- """
- self._tools_to_fix = fixed_tool_arguments
-
- def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
- registry.add_callback(BeforeToolCallEvent, self._fix_tool_arguments)
-
- def _fix_tool_arguments(self, event: BeforeToolCallEvent):
- # If the tool is in our list of parameters, then use those parameters
- if parameters_to_fix := self._tools_to_fix.get(event.tool_use["name"]):
- tool_input: dict[str, Any] = event.tool_use["input"]
- tool_input.update(parameters_to_fix)
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/agents/hooks.ts:fixed_tool_arguments_class"
-```
-
-
-
-For example, to always force the `calculator` tool to use precision of 1 digit:
-
-
-
-
-```python
-fix_parameters = ConstantToolArguments({
- "calculator": {
- "precision": 1,
- }
-})
-
-agent = Agent(tools=[calculator], hooks=[fix_parameters])
-result = agent("What is 2 / 3?")
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/agents/hooks.ts:fixed_tool_arguments_usage"
-```
-
-
-
-### Limit Tool Counts
-
-Useful for preventing runaway tool usage, implementing rate limiting, or enforcing usage quotas. This hook tracks tool invocations per request and replaces tools with error messages when limits are exceeded.
-
-
-
-
-```python
-from strands import tool
-from strands.hooks import HookRegistry, HookProvider, BeforeToolCallEvent, BeforeInvocationEvent
-from threading import Lock
-
-class LimitToolCounts(HookProvider):
- """Limits the number of times tools can be called per agent invocation"""
-
- def __init__(self, max_tool_counts: dict[str, int]):
- """
- Initializer.
-
- Args:
- max_tool_counts: A dictionary mapping tool names to max call counts for
- tools. If a tool is not specified in it, the tool can be called as many
- times as desired
- """
- self.max_tool_counts = max_tool_counts
- self.tool_counts = {}
- self._lock = Lock()
-
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(BeforeInvocationEvent, self.reset_counts)
- registry.add_callback(BeforeToolCallEvent, self.intercept_tool)
-
- def reset_counts(self, event: BeforeInvocationEvent) -> None:
- with self._lock:
- self.tool_counts = {}
-
- def intercept_tool(self, event: BeforeToolCallEvent) -> None:
- tool_name = event.tool_use["name"]
- with self._lock:
- max_tool_count = self.max_tool_counts.get(tool_name)
- tool_count = self.tool_counts.get(tool_name, 0) + 1
- self.tool_counts[tool_name] = tool_count
-
- if max_tool_count and tool_count > max_tool_count:
- event.cancel_tool = (
- f"Tool '{tool_name}' has been invoked too many and is now being throttled. "
- f"DO NOT CALL THIS TOOL ANYMORE "
- )
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/agents/hooks.ts:limit_tool_counts_class"
-```
-
-
-
-For example, to limit the `sleep` tool to 3 invocations per invocation:
-
-
-
-
-```python
-limit_hook = LimitToolCounts(max_tool_counts={"sleep": 3})
-
-agent = Agent(tools=[sleep], hooks=[limit_hook])
-
-# This call will only have 3 successful sleeps
-agent("Sleep 5 times for 10ms each or until you can't anymore")
-# This will sleep successfully again because the count resets every invocation
-agent("Sleep once")
-```
-
-
-
-```typescript
---8<-- "user-guide/concepts/agents/hooks.ts:limit_tool_counts_usage"
-```
-
-
-
-### Model Call Retry
-
-Useful for implementing custom retry logic for model invocations. The `AfterModelCallEvent.retry` field allows hooks to request retries based on any criteria—exceptions, response validation, content quality checks, or any custom logic. This example demonstrates retrying on exceptions with exponential backoff:
-
-
-
-
-```python
-import asyncio
-import logging
-from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, AfterModelCallEvent
-
-logger = logging.getLogger(__name__)
-
-class RetryOnServiceUnavailable(HookProvider):
- """Retry model calls when ServiceUnavailable errors occur."""
-
- def __init__(self, max_retries: int = 3):
- self.max_retries = max_retries
- self.retry_count = 0
-
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(BeforeInvocationEvent, self.reset_counts)
- registry.add_callback(AfterModelCallEvent, self.handle_retry)
-
- def reset_counts(self, event: BeforeInvocationEvent = None) -> None:
- self.retry_count = 0
-
- async def handle_retry(self, event: AfterModelCallEvent) -> None:
- if event.exception:
- if "ServiceUnavailable" in str(event.exception):
- logger.info("ServiceUnavailable encountered")
- if self.retry_count < self.max_retries:
- logger.info("Retrying model call")
- self.retry_count += 1
- event.retry = True
- await asyncio.sleep(2 ** self.retry_count) # Exponential backoff
- else:
- # Reset counts on successful call
- self.reset_counts()
-```
-
-
-
-```ts
-// This feature is not yet available in TypeScript SDK
-```
-
-
-
-For example, to retry up to 3 times on service unavailable errors:
-
-
-
-
-```python
-from strands import Agent
-
-retry_hook = RetryOnServiceUnavailable(max_retries=3)
-agent = Agent(hooks=[retry_hook])
-
-result = agent("What is the capital of France?")
-```
-
-
-
-```ts
-// This feature is not yet available in TypeScript SDK
-```
-
-
-
-### Exception Handling
-
-When a tool raises an exception, the agent converts it to an error result and returns it to the model, allowing the model to adjust its approach and retry. This works well for expected errors like validation failures, but for unexpected errors—assertion failures, configuration errors, or bugs—you may want to fail immediately rather than let the model retry futilely. The `exception` property on `AfterToolCallEvent` provides access to the original exception, enabling hooks to inspect error types and selectively propagate those that shouldn't be retried:
-
-
-
-
-```python
-class PropagateUnexpectedExceptions(HookProvider):
- """Re-raise unexpected exceptions instead of returning them to the model."""
-
- def __init__(self, allowed_exceptions: tuple[type[Exception], ...] = (ValueError,)):
- self.allowed_exceptions = allowed_exceptions
-
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(AfterToolCallEvent, self._check_exception)
-
- def _check_exception(self, event: AfterToolCallEvent) -> None:
- if event.exception is None:
- return # Tool succeeded
- if isinstance(event.exception, self.allowed_exceptions):
- return # Let model retry these
- raise event.exception # Propagate unexpected errors
-```
-
-```python
-# Usage
-agent = Agent(
- model=model,
- tools=[my_tool],
- hooks=[PropagateUnexpectedExceptions(allowed_exceptions=(ValueError, ValidationError))],
-)
-```
-
-
-
-
-```ts
-// This feature is not yet available in TypeScript SDK
-```
-
-
-
-### Tool Call Retry
-
-Useful for implementing custom retry logic for tool invocations. The `AfterToolCallEvent.retry` field allows hooks to request that a tool be re-executed—for example, to handle transient errors, timeouts, or flaky external services. When `retry` is set to `True`, the tool executor discards the current result and invokes the tool again with the same `tool_use_id`.
-
-:::note[Streaming behavior]
-When a tool call is retried, intermediate streaming events (`ToolStreamEvent`) from discarded attempts will have already been emitted to callers. Only the final attempt's `ToolResultEvent` is emitted and added to conversation history. Callers consuming streamed events should be prepared to handle events from discarded attempts.
-:::
-
-
-
-
-```python
-import logging
-from strands.hooks import HookProvider, HookRegistry, AfterToolCallEvent
-
-logger = logging.getLogger(__name__)
-
-class RetryOnToolError(HookProvider):
- """Retry tool calls that fail with errors."""
-
- def __init__(self, max_retries: int = 1):
- self.max_retries = max_retries
- self._attempt_counts: dict[str, int] = {}
-
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(AfterToolCallEvent, self.handle_retry)
-
- def handle_retry(self, event: AfterToolCallEvent) -> None:
- tool_use_id = str(event.tool_use.get("toolUseId", ""))
- tool_name = event.tool_use.get("name", "unknown")
-
- # Track attempts per tool_use_id
- attempt = self._attempt_counts.get(tool_use_id, 0) + 1
- self._attempt_counts[tool_use_id] = attempt
-
- if event.result.get("status") == "error" and attempt <= self.max_retries:
- logger.info(f"Retrying tool '{tool_name}' (attempt {attempt}/{self.max_retries})")
- event.retry = True
- elif event.result.get("status") != "error":
- # Clean up tracking on success
- self._attempt_counts.pop(tool_use_id, None)
-```
-
-
-
-```ts
-// This feature is not yet available in TypeScript SDK
-```
-
-
-
-For example, to retry failed tool calls once:
-
-
-
-
-```python
-from strands import Agent, tool
-
-@tool
-def flaky_api_call(query: str) -> str:
- """Call an external API that sometimes fails.
-
- Args:
- query: The query to send.
- """
- import random
- if random.random() < 0.5:
- raise RuntimeError("Service temporarily unavailable")
- return f"Result for: {query}"
-
-retry_hook = RetryOnToolError(max_retries=1)
-agent = Agent(tools=[flaky_api_call], hooks=[retry_hook])
-
-result = agent("Look up the weather")
-```
-
-
-
-```ts
-// This feature is not yet available in TypeScript SDK
-```
-
-
-
-### Invocation resume
-
-The `AfterInvocationEvent.resume` property enables a hook to trigger a follow-up agent invocation after the current one completes. When you set `resume` to any valid agent input (a string, content blocks, or messages), the agent automatically re-invokes itself with that input instead of returning to the caller. This starts a full new invocation cycle, including firing `BeforeInvocationEvent`.
-
-This is useful for building autonomous looping patterns where the agent continues processing based on its previous result—for example, re-evaluating after tool execution, injecting additional context, or implementing multi-step workflows within a single call.
-
-:::note[Resume input types]
-The `resume` value accepts any valid `AgentInput`: a string, a list of content blocks, a list of messages, or interrupt responses. When the agent is in an interrupt state, you must provide interrupt responses (not a plain string) to resume correctly.
-:::
-
-The following example checks the agent result and triggers one follow-up invocation to ask the model to summarize its work:
-
-
-
-
-```python
-from strands import Agent
-from strands.hooks import AfterInvocationEvent
-
-resume_count = 0
-
-async def summarize_after_tools(event: AfterInvocationEvent):
- """Resume once to ask the model to summarize its work."""
- global resume_count
- if resume_count == 0 and event.result and event.result.stop_reason == "end_turn":
- resume_count += 1
- event.resume = "Now summarize what you just did in one sentence."
-
-agent = Agent()
-agent.add_hook(summarize_after_tools)
-
-# The agent processes the initial request, then automatically
-# performs a second invocation to generate the summary
-result = agent("Look up the weather in Seattle")
-```
-
-
-
-```ts
-// This feature is not yet available in TypeScript SDK
-```
-
-
-
-You can also use `resume` to chain multiple re-invocations. Make sure to include a termination condition to avoid infinite loops:
-
-
-
-
-```python
-from strands import Agent
-from strands.hooks import AfterInvocationEvent
-
-MAX_ITERATIONS = 3
-iteration = 0
-
-async def iterative_refinement(event: AfterInvocationEvent):
- """Re-invoke the agent up to MAX_ITERATIONS times for iterative refinement."""
- global iteration
- if iteration < MAX_ITERATIONS and event.result:
- iteration += 1
- event.resume = f"Review your previous response and improve it. Iteration {iteration} of {MAX_ITERATIONS}."
-
-agent = Agent()
-agent.add_hook(iterative_refinement)
-
-result = agent("Draft a haiku about programming")
-```
-
-
-
-```ts
-// This feature is not yet available in TypeScript SDK
-```
-
-
-
-#### Handling interrupts with resume
-
-The `resume` property integrates with the [interrupt](../../tools/index.md) system. When an agent invocation ends because of an interrupt, a hook can automatically handle the interrupt by resuming with interrupt responses. This avoids returning the interrupt to the caller.
-
-When the agent is in an interrupt state, you must resume with a list of `interruptResponse` objects. Passing a plain string raises a `TypeError`.
-
-
-
-
-```python
-from strands import Agent, tool
-from strands.hooks import AfterInvocationEvent, BeforeToolCallEvent
-
-@tool
-def send_email(to: str, body: str) -> str:
- """Send an email.
-
- Args:
- to: Recipient address.
- body: Email body.
- """
- return f"Email sent to {to}"
-
-def require_approval(event: BeforeToolCallEvent):
- """Interrupt before sending emails to require approval."""
- if event.tool_use["name"] == "send_email":
- event.interrupt("email_approval", reason="Approve this email?")
-
-async def auto_approve(event: AfterInvocationEvent):
- """Automatically approve all interrupted tool calls."""
- if event.result and event.result.stop_reason == "interrupt":
- responses = [
- {"interruptResponse": {"interruptId": intr.id, "response": "approved"}}
- for intr in event.result.interrupts
- ]
- event.resume = responses
-
-agent = Agent(tools=[send_email])
-agent.add_hook(require_approval)
-agent.add_hook(auto_approve)
-
-# The interrupt is handled automatically by the hook—
-# the caller receives the final result directly
-result = agent("Send an email to alice@example.com saying hello")
-```
-
-
-
-```ts
-// This feature is not yet available in TypeScript SDK
-```
-
-
-
-## HookProvider Protocol
-
-For advanced use cases, you can implement the `HookProvider` protocol to create objects that register multiple callbacks at once. This is useful when building reusable hook collections without the full plugin infrastructure:
-
-
-
-
-```python
-from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, AfterInvocationEvent
-
-class RequestLogger(HookProvider):
- def register_hooks(self, registry: HookRegistry) -> None:
- registry.add_callback(BeforeInvocationEvent, self.log_start)
- registry.add_callback(AfterInvocationEvent, self.log_end)
-
- def log_start(self, event: BeforeInvocationEvent) -> None:
- print(f"Request started for agent: {event.agent.name}")
-
- def log_end(self, event: AfterInvocationEvent) -> None:
- print(f"Request completed for agent: {event.agent.name}")
-
-# Pass via hooks parameter
-agent = Agent(hooks=[RequestLogger()])
-
-# Or add after creation
-agent.hooks.add_hook(RequestLogger())
-```
-
-For most use cases, [Plugins](../plugins/index.md) provide a more convenient way to bundle multiple hooks with additional features like auto-discovery and tool registration.
-
-
-
-
-:::note[TypeScript SDK]
-The TypeScript SDK does not export a `HookProvider` interface. Instead, use the [Plugin](../plugins/index.md) class to bundle multiple hooks together. The `Plugin` class provides `initAgent()` for registering hooks and `getTools()` for providing tools.
-:::
-
-```typescript
---8<-- "user-guide/concepts/plugins/index.ts:plugin_for_hooks"
-```
-
-
diff --git a/src/content/docs/user-guide/concepts/agents/hooks/cookbook.mdx b/src/content/docs/user-guide/concepts/agents/hooks/cookbook.mdx
new file mode 100644
index 000000000..9395561c5
--- /dev/null
+++ b/src/content/docs/user-guide/concepts/agents/hooks/cookbook.mdx
@@ -0,0 +1,521 @@
+---
+title: Hooks cookbook
+description: "Copy-paste hook patterns for common agent behaviors: fixed arguments, rate limiting, retries, and autonomous looping."
+sidebar:
+ order: 2
+---
+
+Practical hook patterns you can copy and adapt. Each recipe includes a complete implementation and a usage example. See [hooks concepts](./index) for background and [using hooks](./usage) for registration basics.
+
+## Fix tool arguments
+
+Force specific tools to always use predetermined parameter values. Useful for enforcing security policies or maintaining consistency across invocations.
+
+
+
+
+```python
+from typing import Any
+from strands.hooks import HookProvider, HookRegistry, BeforeToolCallEvent
+
+
+class ConstantToolArguments(HookProvider):
+ """Use constant argument values for specific parameters of a tool."""
+
+ def __init__(self, fixed_tool_arguments: dict[str, dict[str, Any]]):
+ """Initialize fixed parameter values for tools.
+
+ Args:
+ fixed_tool_arguments: Maps tool names to dictionaries of parameter
+ names and their fixed values. These values override any values
+ the agent provides when the tool is invoked.
+ """
+ self._tools_to_fix = fixed_tool_arguments
+
+ def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
+ registry.add_callback(BeforeToolCallEvent, self._fix_tool_arguments)
+
+ def _fix_tool_arguments(self, event: BeforeToolCallEvent) -> None:
+ if parameters_to_fix := self._tools_to_fix.get(event.tool_use["name"]):
+ tool_input: dict[str, Any] = event.tool_use["input"]
+ tool_input.update(parameters_to_fix)
+```
+
+
+
+
+
+```typescript
+import { Agent, FunctionTool } from '@strands-agents/sdk'
+import type { LocalAgent, Plugin } from '@strands-agents/sdk'
+import { BeforeToolCallEvent } from '@strands-agents/sdk'
+
+class ConstantToolArguments implements Plugin {
+ private fixedToolArguments: Record>
+
+ constructor(fixedToolArguments: Record>) {
+ this.fixedToolArguments = fixedToolArguments
+ }
+
+ name = 'constant-tool-arguments'
+
+ initAgent(agent: LocalAgent): void {
+ agent.addHook(BeforeToolCallEvent, (ev) => this.fixToolArguments(ev))
+ }
+
+ private fixToolArguments(event: BeforeToolCallEvent): void {
+ const parametersToFix = this.fixedToolArguments[event.toolUse.name]
+ if (parametersToFix) {
+ const toolInput = event.toolUse.input as Record
+ Object.assign(toolInput, parametersToFix)
+ }
+ }
+}
+```
+
+
+
+
+Force the `calculator` tool to always use a precision of 1:
+
+
+
+
+```python
+from strands import Agent
+
+fix_parameters = ConstantToolArguments({
+ "calculator": {
+ "precision": 1,
+ }
+})
+
+agent = Agent(tools=[calculator], hooks=[fix_parameters])
+result = agent("What is 2 / 3?")
+```
+
+
+
+
+
+```typescript
+const fixParameters = new ConstantToolArguments({
+ calculator: {
+ precision: 1,
+ },
+})
+
+const agent = new Agent({ tools: [calculator], plugins: [fixParameters] })
+const result = await agent.invoke('What is 2 / 3?')
+```
+
+
+
+
+## Limit tool call counts
+
+Prevent runaway tool usage by capping calls per invocation. Useful for rate limiting, usage quotas, and preventing infinite loops.
+
+
+
+
+```python
+from typing import Any
+from threading import Lock
+
+from strands.hooks import HookProvider, HookRegistry, BeforeToolCallEvent, BeforeInvocationEvent
+
+
+class LimitToolCounts(HookProvider):
+ """Limit the number of times each tool can be called per agent invocation."""
+
+ def __init__(self, max_tool_counts: dict[str, int]):
+ """Initialize with per-tool call limits.
+
+ Args:
+ max_tool_counts: Maps tool names to maximum allowed call counts.
+ Tools not listed have no limit.
+ """
+ self.max_tool_counts = max_tool_counts
+ self.tool_counts: dict[str, int] = {}
+ self._lock = Lock()
+
+ def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
+ registry.add_callback(BeforeInvocationEvent, self.reset_counts)
+ registry.add_callback(BeforeToolCallEvent, self.intercept_tool)
+
+ def reset_counts(self, event: BeforeInvocationEvent) -> None:
+ with self._lock:
+ self.tool_counts = {}
+
+ def intercept_tool(self, event: BeforeToolCallEvent) -> None:
+ tool_name = event.tool_use["name"]
+ with self._lock:
+ max_tool_count = self.max_tool_counts.get(tool_name)
+ tool_count = self.tool_counts.get(tool_name, 0) + 1
+ self.tool_counts[tool_name] = tool_count
+
+ if max_tool_count and tool_count > max_tool_count:
+ event.cancel_tool = (
+ f"Tool '{tool_name}' has been invoked too many times and is now being throttled. "
+ f"DO NOT CALL THIS TOOL ANYMORE"
+ )
+```
+
+
+
+
+
+```typescript
+import { Agent, FunctionTool } from '@strands-agents/sdk'
+import type { LocalAgent, Plugin } from '@strands-agents/sdk'
+import { BeforeInvocationEvent, BeforeToolCallEvent } from '@strands-agents/sdk'
+
+class LimitToolCounts implements Plugin {
+ private maxToolCounts: Record
+ private toolCounts: Record = {}
+
+ constructor(maxToolCounts: Record) {
+ this.maxToolCounts = maxToolCounts
+ }
+
+ name = 'limit-tool-counts'
+
+ initAgent(agent: LocalAgent): void {
+ agent.addHook(BeforeInvocationEvent, () => this.resetCounts())
+ agent.addHook(BeforeToolCallEvent, (event) => this.interceptTool(event))
+ }
+
+ private resetCounts(): void {
+ this.toolCounts = {}
+ }
+
+ private interceptTool(event: BeforeToolCallEvent): void {
+ const toolName = event.toolUse.name
+ const maxToolCount = this.maxToolCounts[toolName]
+ const toolCount = (this.toolCounts[toolName] ?? 0) + 1
+ this.toolCounts[toolName] = toolCount
+
+ if (maxToolCount !== undefined && toolCount > maxToolCount) {
+ event.cancel =
+ `Tool '${toolName}' has been invoked too many times and is now being throttled. ` +
+ `DO NOT CALL THIS TOOL ANYMORE`
+ }
+ }
+}
+```
+
+
+
+
+Limit the `sleep` tool to 3 calls per invocation:
+
+
+
+
+```python
+from strands import Agent
+
+limit_hook = LimitToolCounts(max_tool_counts={"sleep": 3})
+
+agent = Agent(tools=[sleep], hooks=[limit_hook])
+
+# Only 3 successful sleeps occur
+agent("Sleep 5 times for 10ms each or until you can't anymore")
+# Count resets on the next invocation
+agent("Sleep once")
+```
+
+
+
+
+
+```typescript
+const limitPlugin = new LimitToolCounts({ sleep: 3 })
+
+const agent = new Agent({ tools: [sleep], plugins: [limitPlugin] })
+
+// Only 3 successful sleeps occur
+await agent.invoke('Sleep 5 times for 10ms each or until you can\'t anymore')
+// Count resets on the next invocation
+await agent.invoke('Sleep once')
+```
+
+
+
+
+## Retry model calls
+
+Retry model calls on transient errors with exponential backoff.
+
+
+
+
+```python
+import asyncio
+import logging
+from typing import Any
+
+from strands import Agent
+from strands.hooks import HookProvider, HookRegistry, BeforeInvocationEvent, AfterModelCallEvent
+
+logger = logging.getLogger(__name__)
+
+
+class RetryOnServiceUnavailable(HookProvider):
+ """Retry model calls when ServiceUnavailable errors occur."""
+
+ def __init__(self, max_retries: int = 3):
+ self.max_retries = max_retries
+ self.retry_count = 0
+
+ def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
+ registry.add_callback(BeforeInvocationEvent, self.reset_counts)
+ registry.add_callback(AfterModelCallEvent, self.handle_retry)
+
+ def reset_counts(self, event: BeforeInvocationEvent) -> None:
+ self.retry_count = 0
+
+ async def handle_retry(self, event: AfterModelCallEvent) -> None:
+ # Hook callbacks can be async when they need to await
+ if event.exception:
+ if "ServiceUnavailable" in str(event.exception):
+ logger.info("ServiceUnavailable encountered")
+ if self.retry_count < self.max_retries:
+ logger.info("Retrying model call")
+ self.retry_count += 1
+ event.retry = True
+ await asyncio.sleep(2 ** self.retry_count)
+ else:
+ self.retry_count = 0
+```
+
+
+
+
+Use it:
+
+
+
+
+```python
+from strands import Agent
+
+retry_hook = RetryOnServiceUnavailable(max_retries=3)
+agent = Agent(hooks=[retry_hook])
+
+result = agent("What is the capital of France?")
+```
+
+
+
+
+## Retry tool calls
+
+Re-execute failed tool calls automatically.
+
+:::note[Streaming behavior]
+When a tool call retries, intermediate streaming events from discarded attempts will have already been emitted. Only the final attempt's result is added to the conversation. Callers consuming streamed events should handle events from discarded attempts.
+:::
+
+
+
+
+```python
+import logging
+from typing import Any
+
+from strands.hooks import HookProvider, HookRegistry, AfterToolCallEvent
+
+logger = logging.getLogger(__name__)
+
+
+class RetryOnToolError(HookProvider):
+ """Retry tool calls that fail with errors."""
+
+ def __init__(self, max_retries: int = 1):
+ self.max_retries = max_retries
+ self._attempt_counts: dict[str, int] = {}
+
+ def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
+ registry.add_callback(AfterToolCallEvent, self.handle_retry)
+
+ def handle_retry(self, event: AfterToolCallEvent) -> None:
+ tool_use_id = str(event.tool_use.get("toolUseId", ""))
+ tool_name = event.tool_use.get("name", "unknown")
+
+ attempt = self._attempt_counts.get(tool_use_id, 0) + 1
+ self._attempt_counts[tool_use_id] = attempt
+
+ if event.result.get("status") == "error" and attempt <= self.max_retries:
+ logger.info(f"Retrying tool '{tool_name}' (attempt {attempt}/{self.max_retries})")
+ event.retry = True
+ elif event.result.get("status") != "error":
+ self._attempt_counts.pop(tool_use_id, None)
+```
+
+
+
+
+Retry a flaky API call once before giving up:
+
+
+
+
+```python
+from strands import Agent, tool
+
+
+@tool
+def flaky_api_call(query: str) -> str:
+ """Call an external API that sometimes fails.
+
+ Args:
+ query: The query to send.
+ """
+ import random
+ if random.random() < 0.5:
+ raise RuntimeError("Service temporarily unavailable")
+ return f"Result for: {query}"
+
+
+retry_hook = RetryOnToolError(max_retries=1)
+agent = Agent(tools=[flaky_api_call], hooks=[retry_hook])
+
+result = agent("Look up the weather")
+```
+
+
+
+
+## Resume after invocation
+
+Trigger follow-up agent invocations automatically after the current one completes. Useful for autonomous looping and iterative refinement.
+
+:::note
+Python only. TypeScript resume is tracked in [sdk-typescript#463](https://github.com/strands-agents/sdk-typescript/issues/463).
+:::
+
+These examples use bare functions with `agent.add_hook()` instead of `HookProvider` classes for brevity. For production use, wrap the state in a class to avoid global variables.
+
+Summarize after tools (single resume):
+
+
+
+
+```python
+from strands import Agent
+from strands.hooks import AfterInvocationEvent
+
+resume_count = 0
+
+
+async def summarize_after_tools(event: AfterInvocationEvent):
+ """Resume once to ask the model to summarize its work."""
+ global resume_count
+ if resume_count == 0 and event.result and event.result.stop_reason == "end_turn":
+ resume_count += 1
+ event.resume = "Now summarize what you just did in one sentence."
+
+
+agent = Agent()
+agent.add_hook(summarize_after_tools)
+
+# The agent processes the request, then automatically
+# performs a second invocation to generate the summary
+result = agent("Look up the weather in Seattle")
+```
+
+
+
+
+Refine iteratively up to N times with a termination condition:
+
+:::caution
+Always include a termination condition to avoid infinite loops.
+:::
+
+
+
+
+```python
+from strands import Agent
+from strands.hooks import AfterInvocationEvent
+
+MAX_ITERATIONS = 3
+iteration = 0
+
+
+async def iterative_refinement(event: AfterInvocationEvent):
+ """Re-invoke the agent up to MAX_ITERATIONS times for iterative refinement."""
+ global iteration
+ if iteration < MAX_ITERATIONS and event.result:
+ iteration += 1
+ event.resume = f"Review your previous response and improve it. Iteration {iteration} of {MAX_ITERATIONS}."
+
+
+agent = Agent()
+agent.add_hook(iterative_refinement)
+
+result = agent("Draft a haiku about programming")
+```
+
+
+
+
+## Handle interrupts with resume
+
+Automatically approve or handle interrupted tool calls without returning to the caller. When an agent invocation ends because of an interrupt, a hook can resume with interrupt responses to continue execution.
+
+When the agent is in an interrupt state, resume with a list of `interruptResponse` objects. Passing a plain string raises a `TypeError`.
+
+:::note
+Python only. TypeScript interrupts are tracked in [sdk-typescript#479](https://github.com/strands-agents/sdk-typescript/issues/479) and [sdk-typescript#477](https://github.com/strands-agents/sdk-typescript/issues/477).
+:::
+
+
+
+
+```python
+from strands import Agent, tool
+from strands.hooks import AfterInvocationEvent, BeforeToolCallEvent
+
+
+@tool
+def send_email(to: str, body: str) -> str:
+ """Send an email.
+
+ Args:
+ to: Recipient address.
+ body: Email body.
+ """
+ return f"Email sent to {to}"
+
+
+def require_approval(event: BeforeToolCallEvent):
+ """Interrupt before sending emails to require approval."""
+ if event.tool_use["name"] == "send_email":
+ event.interrupt("email_approval", reason="Approve this email?")
+
+
+async def auto_approve(event: AfterInvocationEvent):
+ """Automatically approve all interrupted tool calls."""
+ if event.result and event.result.stop_reason == "interrupt":
+ responses = [
+ {"interruptResponse": {"interruptId": intr.id, "response": "approved"}}
+ for intr in event.result.interrupts
+ ]
+ event.resume = responses
+
+
+agent = Agent(tools=[send_email])
+agent.add_hook(require_approval)
+agent.add_hook(auto_approve)
+
+# The interrupt is handled automatically by the hook,
+# the caller receives the final result directly
+result = agent("Send an email to alice@example.com saying hello")
+```
+
+
+
diff --git a/src/content/docs/user-guide/concepts/agents/hooks/index.mdx b/src/content/docs/user-guide/concepts/agents/hooks/index.mdx
new file mode 100644
index 000000000..bf420f9a3
--- /dev/null
+++ b/src/content/docs/user-guide/concepts/agents/hooks/index.mdx
@@ -0,0 +1,344 @@
+---
+title: Hooks
+description: "Observe and modify agent behavior at runtime with lifecycle hooks that fire before and after key events in the agent loop."
+sidebar:
+ order: 0
+---
+
+You use hooks to observe and modify agent behavior at runtime. Hooks fire at defined points in the agent loop: before and after model calls, tool calls, and full agent invocations. They are code, not prompt instructions, so they enforce behavior reliably.
+
+This page covers the mental model for hooks. See the [usage guide](./usage) for registration patterns and the [cookbook](./cookbook) for ready-made recipes.
+
+## How hooks work
+
+Hooks subscribe to lifecycle events. Each event type has a before/after pair (for example, `BeforeToolCallEvent` and `AfterToolCallEvent`). When the agent reaches that point in the loop, it fires the event and passes an event object to every registered callback.
+
+Callbacks receive a strongly typed event object carrying context for that stage of the lifecycle. Most event properties are read-only, but specific events expose writable properties that let you modify behavior: cancel a tool call, retry a model call, or rewrite a result.
+
+Here is the simplest case: a callback that prints the tool name before every tool call.
+
+
+
+
+```python
+from strands import Agent
+from strands.hooks import BeforeToolCallEvent
+
+agent = Agent()
+
+def log_tool_name(event: BeforeToolCallEvent) -> None:
+ print(f"Calling tool: {event.tool_use['name']}")
+
+agent.add_hook(log_tool_name)
+```
+
+
+
+
+```typescript
+import { Agent, BeforeToolCallEvent } from '@strands-agents/sdk'
+
+const agent = new Agent()
+
+agent.addHook(BeforeToolCallEvent, (event) => {
+ console.log(`Calling tool: ${event.toolUse.name}`)
+})
+```
+
+
+
+
+The callback runs in the agent loop. It receives the event, reads (or writes) properties, and returns. Both Python and TypeScript support async callbacks (`async def` / `Promise`).
+
+## Hook event lifecycle
+
+### Single-agent lifecycle
+
+The following diagram shows when hook events fire during a typical agent invocation where tools are called.
+
+
+
+
+```mermaid
+flowchart LR
+ subgraph Start["Request Start Events"]
+ direction TB
+ BeforeInvocationEvent["BeforeInvocationEvent"]
+ StartMessage["MessageAddedEvent"]
+ BeforeInvocationEvent --> StartMessage
+ end
+ subgraph Model["Model Events"]
+ direction TB
+ BeforeModelCallEvent["BeforeModelCallEvent"]
+ AfterModelCallEvent["AfterModelCallEvent"]
+ ModelMessage["MessageAddedEvent"]
+ BeforeModelCallEvent --> AfterModelCallEvent
+ AfterModelCallEvent --> ModelMessage
+ end
+ subgraph Tool["Tool Events"]
+ direction TB
+ BeforeToolCallEvent["BeforeToolCallEvent"]
+ AfterToolCallEvent["AfterToolCallEvent"]
+ ToolMessage["MessageAddedEvent"]
+ BeforeToolCallEvent --> AfterToolCallEvent
+ AfterToolCallEvent --> ToolMessage
+ end
+ subgraph End["Request End Events"]
+ direction TB
+ AfterInvocationEvent["AfterInvocationEvent"]
+ end
+Start --> Model
+Model <--> Tool
+Tool --> End
+```
+
+
+
+
+The TypeScript lifecycle includes additional streaming events between the before/after pairs.
+
+```mermaid
+flowchart LR
+ subgraph Start["Request Start Events"]
+ direction TB
+ BeforeInvocationEvent["BeforeInvocationEvent"]
+ StartMessage["MessageAddedEvent"]
+ BeforeInvocationEvent --> StartMessage
+ end
+ subgraph Model["Model Events"]
+ direction TB
+ BeforeModelCallEvent["BeforeModelCallEvent"]
+ ModelStreamUpdateEvent["ModelStreamUpdateEvent"]
+ ContentBlockEvent["ContentBlockEvent"]
+ ModelMessageEvent["ModelMessageEvent"]
+ AfterModelCallEvent["AfterModelCallEvent"]
+ ModelMessage["MessageAddedEvent"]
+ BeforeModelCallEvent --> ModelStreamUpdateEvent
+ ModelStreamUpdateEvent --> ContentBlockEvent
+ ContentBlockEvent --> ModelMessageEvent
+ ModelMessageEvent --> AfterModelCallEvent
+ AfterModelCallEvent --> ModelMessage
+ end
+ subgraph Tool["Tool Events"]
+ direction TB
+ BeforeToolCallEvent["BeforeToolCallEvent"]
+ ToolStreamUpdateEvent["ToolStreamUpdateEvent"]
+ ToolResultEvent["ToolResultEvent"]
+ AfterToolCallEvent["AfterToolCallEvent"]
+ ToolMessage["MessageAddedEvent"]
+ BeforeToolCallEvent --> ToolStreamUpdateEvent
+ ToolStreamUpdateEvent --> ToolResultEvent
+ ToolResultEvent --> AfterToolCallEvent
+ AfterToolCallEvent --> ToolMessage
+ end
+ subgraph End["Request End Events"]
+ direction TB
+ AgentResultEvent["AgentResultEvent"]
+ AfterInvocationEvent["AfterInvocationEvent"]
+ AgentResultEvent --> AfterInvocationEvent
+ end
+Start --> Model
+Model <--> Tool
+Tool --> End
+```
+
+
+
+
+### Multi-agent lifecycle
+
+The following diagram shows when multi-agent hook events fire during orchestrator execution.
+
+
+
+
+```mermaid
+flowchart LR
+subgraph Init["Initialization"]
+ direction TB
+ MultiAgentInitializedEvent["MultiAgentInitializedEvent"]
+end
+subgraph Invocation["Invocation Lifecycle"]
+ direction TB
+ BeforeMultiAgentInvocationEvent["BeforeMultiAgentInvocationEvent"]
+ AfterMultiAgentInvocationEvent["AfterMultiAgentInvocationEvent"]
+ BeforeMultiAgentInvocationEvent --> NodeExecution
+ NodeExecution --> AfterMultiAgentInvocationEvent
+end
+subgraph NodeExecution["Node Execution (Repeated)"]
+ direction TB
+ BeforeNodeCallEvent["BeforeNodeCallEvent"]
+ AfterNodeCallEvent["AfterNodeCallEvent"]
+ BeforeNodeCallEvent --> AfterNodeCallEvent
+end
+Init --> Invocation
+```
+
+
+
+
+The TypeScript multi-agent lifecycle includes streaming, result, and handoff events within node execution.
+
+```mermaid
+flowchart LR
+subgraph Init["Initialization"]
+ direction TB
+ MultiAgentInitializedEvent["MultiAgentInitializedEvent"]
+end
+subgraph Invocation["Invocation Lifecycle"]
+ direction TB
+ BeforeMultiAgentInvocationEvent["BeforeMultiAgentInvocationEvent"]
+ AfterMultiAgentInvocationEvent["AfterMultiAgentInvocationEvent"]
+ MultiAgentResultEvent["MultiAgentResultEvent"]
+ BeforeMultiAgentInvocationEvent --> NodeExecution
+ NodeExecution --> AfterMultiAgentInvocationEvent
+ AfterMultiAgentInvocationEvent --> MultiAgentResultEvent
+end
+subgraph NodeExecution["Node Execution (Repeated)"]
+ direction TB
+ BeforeNodeCallEvent["BeforeNodeCallEvent"]
+ NodeStreamUpdateEvent["NodeStreamUpdateEvent"]
+ AfterNodeCallEvent["AfterNodeCallEvent"]
+ NodeResultEvent["NodeResultEvent"]
+ MultiAgentHandoffEvent["MultiAgentHandoffEvent"]
+ BeforeNodeCallEvent --> NodeStreamUpdateEvent
+ NodeStreamUpdateEvent --> AfterNodeCallEvent
+ AfterNodeCallEvent --> NodeResultEvent
+ NodeResultEvent --> MultiAgentHandoffEvent
+end
+Init --> Invocation
+```
+
+
+
+
+## Available events
+
+TypeScript includes additional streaming events (`ModelStreamUpdateEvent`, `ContentBlockEvent`, `ToolStreamUpdateEvent`, and others) not present in Python. Features marked Python-only in the [cookbook](./cookbook) are being added to TypeScript.
+
+### Single-agent events
+
+
+
+
+| Event | Description |
+|---|---|
+| `AgentInitializedEvent` | Fired when the agent finishes construction at the end of its constructor |
+| `BeforeInvocationEvent` | Fired at the start of a new agent invocation |
+| `AfterInvocationEvent` | Fired at the end of an invocation, regardless of success or failure. Uses reverse callback ordering |
+| `MessageAddedEvent` | Fired when a message is added to the agent's conversation messages |
+| `BeforeModelCallEvent` | Fired before the model is called for inference |
+| `AfterModelCallEvent` | Fired after model calling completes. Uses reverse callback ordering |
+| `BeforeToolCallEvent` | Fired before a tool is called |
+| `AfterToolCallEvent` | Fired after tool calling completes. Uses reverse callback ordering |
+
+
+
+
+All events extend `HookableEvent`, making them both streamable via `agent.stream()` and subscribable via hook callbacks.
+
+| Event | Description |
+|---|---|
+| `InitializedEvent` | Fired when the agent finishes construction at the end of its constructor |
+| `BeforeInvocationEvent` | Fired at the start of a new agent invocation |
+| `AfterInvocationEvent` | Fired at the end of an invocation, regardless of success or failure. Uses reverse callback ordering |
+| `MessageAddedEvent` | Fired when a message is added to the agent's conversation messages |
+| `BeforeModelCallEvent` | Fired before the model is called for inference |
+| `AfterModelCallEvent` | Fired after model calling completes. Uses reverse callback ordering |
+| `ModelStreamUpdateEvent` | Wraps each transient streaming delta from the model during inference. Access via `.event` |
+| `ContentBlockEvent` | Wraps a fully assembled content block (TextBlock, ToolUseBlock, ReasoningBlock). Access via `.contentBlock` |
+| `ModelMessageEvent` | Wraps the complete model message after all blocks are assembled. Access via `.message` |
+| `BeforeToolsEvent` | Fired before tools are called in a batch |
+| `AfterToolsEvent` | Fired after tools are called in a batch. Uses reverse callback ordering |
+| `BeforeToolCallEvent` | Fired before a tool is called |
+| `AfterToolCallEvent` | Fired after tool calling completes. Uses reverse callback ordering |
+| `ToolStreamUpdateEvent` | Wraps streaming progress events from tool calling. Access via `.event` |
+| `ToolResultEvent` | Wraps a completed tool result. Access via `.result` |
+| `AgentResultEvent` | Wraps the final agent result at the end of the invocation. Access via `.result` |
+
+
+
+
+### Multi-agent events
+
+
+
+
+| Event | Description |
+|---|---|
+| `MultiAgentInitializedEvent` | Fired when the multi-agent orchestrator is initialized |
+| `BeforeMultiAgentInvocationEvent` | Fired before orchestrator execution starts |
+| `AfterMultiAgentInvocationEvent` | Fired after orchestrator execution completes. Uses reverse callback ordering |
+| `BeforeNodeCallEvent` | Fired before individual node execution starts |
+| `AfterNodeCallEvent` | Fired after individual node execution completes. Uses reverse callback ordering |
+
+
+
+
+| Event | Description |
+|---|---|
+| `MultiAgentInitializedEvent` | Fired when the multi-agent orchestrator finishes initialization |
+| `BeforeMultiAgentInvocationEvent` | Fired before orchestrator execution starts |
+| `AfterMultiAgentInvocationEvent` | Fired after orchestrator execution completes. Uses reverse callback ordering |
+| `BeforeNodeCallEvent` | Fired before individual node execution starts |
+| `NodeStreamUpdateEvent` | Wraps an inner streaming event from a node with the node's identity. Access via `.event` |
+| `NodeCancelEvent` | Fired when a node is cancelled via `BeforeNodeCallEvent.cancel` |
+| `AfterNodeCallEvent` | Fired after individual node execution completes. Uses reverse callback ordering |
+| `NodeResultEvent` | Wraps a completed node result. Access via `.result` |
+| `MultiAgentHandoffEvent` | Fired when execution transitions between nodes |
+| `MultiAgentResultEvent` | Wraps the final multi-agent result at the end of orchestration. Access via `.result` |
+
+
+
+
+## Modifiable event properties
+
+Most event properties are read-only. The following events expose writable properties that change agent behavior.
+
+
+
+
+| Event | Writable property | Effect | Cookbook recipe |
+|---|---|---|---|
+| `BeforeToolCallEvent` | `cancel_tool` | Cancels tool calling with a message returned to the model | [Limit tool call counts](./cookbook#limit-tool-call-counts) |
+| `BeforeToolCallEvent` | `selected_tool` | Replaces the tool to be called | |
+| `BeforeToolCallEvent` | `tool_use` | Modifies tool parameters before calling | [Fix tool arguments](./cookbook#fix-tool-arguments) |
+| `AfterModelCallEvent` | `retry` | Retries the model call | [Retry model calls](./cookbook#retry-model-calls) |
+| `AfterToolCallEvent` | `result` | Rewrites the tool result | |
+| `AfterToolCallEvent` | `retry` | Retries the tool call | [Retry tool calls](./cookbook#retry-tool-calls) |
+| `AfterInvocationEvent` | `resume` | Triggers a follow-up invocation with new input | [Resume after invocation](./cookbook#resume-after-invocation) |
+
+You can also read `AfterToolCallEvent.exception` to inspect the original error if the tool raised one.
+
+
+
+
+| Event | Writable property | Effect |
+|---|---|---|
+| `BeforeToolsEvent` | `cancel` | Cancels all tool calls in a batch with a message |
+| `BeforeToolCallEvent` | `cancel` | Cancels tool calling with a message returned to the model |
+| `AfterModelCallEvent` | `retry` | Retries the model call |
+| `AfterToolCallEvent` | `retry` | Retries the tool call |
+
+:::note
+TypeScript does not yet support `selected_tool` replacement, `result` modification, or `resume`. These features are being added.
+:::
+
+
+
+
+## When hooks are the wrong choice
+
+Hooks run synchronously in the agent loop. Every millisecond a hook spends is a millisecond added to the agent's response time. Keep this in mind when deciding where to put logic.
+
+**Long-running checks add latency.** If your hook makes a database lookup or API call, that latency applies to every model call or tool call (depending on the event). For expensive validation, consider these alternatives:
+
+- Cache results in the hook's instance state and refresh periodically
+- Move the check into the tool itself, where it runs only when that tool is called
+- Use async patterns (Python hooks support `async def` callbacks)
+
+**Hooks vs. prompts.** Prompts suggest behavior to the model. Hooks enforce it. A system prompt saying "never call the delete tool" is guidance the model might ignore under pressure. A `BeforeToolCallEvent` hook that cancels `delete` calls is a guarantee. Use prompts for guidance, hooks for guarantees.
+
+**Hooks vs. guardrails.** Hooks are a mechanism. Guardrails are a policy. You implement guardrails using hooks (among other things). A "PII detection guardrail" is a policy decision; the `BeforeToolCallEvent` callback that scans inputs and cancels calls containing PII is the hook that enforces it.
+
+**Why lifecycle hooks and not middleware chains.** We chose lifecycle hooks over middleware chains because hooks compose without ordering dependencies. In a middleware chain, the order you register middleware determines what runs first, and changing the order changes behavior. With hooks, every callback for a given event type runs independently. Before-event callbacks run in registration order; after-event callbacks run in reverse order (unwinding). This makes it safe to combine hooks from multiple plugins without worrying about interference.
diff --git a/src/content/docs/user-guide/concepts/agents/hooks/usage.mdx b/src/content/docs/user-guide/concepts/agents/hooks/usage.mdx
new file mode 100644
index 000000000..9cec3378e
--- /dev/null
+++ b/src/content/docs/user-guide/concepts/agents/hooks/usage.mdx
@@ -0,0 +1,369 @@
+---
+title: Using hooks
+description: "Register hook callbacks on agents and orchestrators to observe and modify behavior during the agent loop."
+sidebar:
+ order: 1
+---
+
+This guide covers how to register hooks on agents, bundle hooks with plugins, pass context through invocation state, and use hooks with multi-agent orchestrators. It assumes familiarity with [hooks concepts](./index) and a working agent.
+
+## Register a hook callback
+
+Register a callback for a specific event with `agent.add_hook()` (Python) or `agent.addHook()` (TypeScript).
+
+
+
+
+Add a type hint to the callback parameter and the event type is inferred automatically. Or pass the event class explicitly:
+
+```python
+from strands import Agent
+from strands.hooks import BeforeInvocationEvent, BeforeToolCallEvent
+
+agent = Agent()
+
+# Type-inferred registration (recommended)
+def on_invocation(event: BeforeInvocationEvent) -> None:
+ print(f"Invocation starting for agent: {event.agent.name}")
+
+agent.add_hook(on_invocation)
+
+# Explicit registration with event class
+def on_tool_call(event: BeforeToolCallEvent) -> None:
+ print(f"Tool called: {event.tool_use['name']}")
+
+agent.add_hook(on_tool_call, BeforeToolCallEvent)
+```
+
+
+
+
+TypeScript always requires the event class as the first argument:
+
+```typescript
+import { Agent } from '@strands-agents/sdk'
+import { BeforeInvocationEvent, BeforeToolCallEvent } from '@strands-agents/sdk'
+
+const agent = new Agent()
+
+agent.addHook(BeforeInvocationEvent, (event) => {
+ console.log('Invocation starting')
+})
+
+agent.addHook(BeforeToolCallEvent, (event) => {
+ console.log(`Tool called: ${event.toolUse.name}`)
+})
+```
+
+
+
+
+## Bundle hooks with plugins
+
+Group related hooks into a single unit with the `Plugin` class. See [Plugins](../../plugins/index.md) for the full plugin API.
+
+
+
+
+Decorate methods with `@hook`. The event type is inferred from the type hint:
+
+```python
+from strands import Agent
+from strands.plugins import Plugin, hook
+from strands.hooks import BeforeToolCallEvent, AfterToolCallEvent
+
+class LoggingPlugin(Plugin):
+ name = "logging-plugin"
+
+ @hook
+ def log_before(self, event: BeforeToolCallEvent) -> None:
+ print(f"Calling: {event.tool_use['name']} with {event.tool_use['input']}")
+
+ @hook
+ def log_after(self, event: AfterToolCallEvent) -> None:
+ print(f"Completed: {event.tool_use['name']}")
+
+agent = Agent(plugins=[LoggingPlugin()])
+agent("Summarize today's news")
+```
+
+
+
+
+Register hooks manually in `initAgent()`:
+
+```typescript
+import { Agent } from '@strands-agents/sdk'
+import type { LocalAgent, Plugin } from '@strands-agents/sdk'
+import { BeforeToolCallEvent, AfterToolCallEvent } from '@strands-agents/sdk'
+
+class LoggingPlugin implements Plugin {
+ name = 'logging-plugin'
+
+ initAgent(agent: LocalAgent): void {
+ agent.addHook(BeforeToolCallEvent, (event) => {
+ console.log(`Calling: ${event.toolUse.name}`)
+ })
+
+ agent.addHook(AfterToolCallEvent, (event) => {
+ console.log(`Completed: ${event.toolUse.name}`)
+ })
+ }
+}
+
+const agent = new Agent({ plugins: [new LoggingPlugin()] })
+```
+
+
+
+
+## Pass context through invocation state
+
+Pass request-scoped data (user IDs, database connections, configuration) into hooks via `invocation_state`. Add custom values as keyword arguments when invoking the agent:
+
+
+
+
+```python
+import sqlite3
+from strands import Agent
+from strands.hooks import BeforeToolCallEvent
+
+def audit_tool_calls(event: BeforeToolCallEvent) -> None:
+ user_id = event.invocation_state.get("user_id", "unknown")
+ db = event.invocation_state.get("db")
+ tool_name = event.tool_use["name"]
+
+ print(f"[audit] user={user_id} tool={tool_name}")
+
+ if db:
+ cursor = db.cursor()
+ cursor.execute(
+ "INSERT INTO audit_log (user_id, tool_name) VALUES (?, ?)",
+ (user_id, tool_name),
+ )
+ db.commit()
+
+agent = Agent()
+agent.add_hook(audit_tool_calls)
+
+db_conn = sqlite3.connect(":memory:")
+db_conn.execute("CREATE TABLE audit_log (user_id TEXT, tool_name TEXT)")
+
+result = agent(
+ "Look up the weather in Portland",
+ user_id="user-42",
+ db=db_conn,
+)
+```
+
+
+
+
+Single-agent invocation state is not yet available in the TypeScript SDK. For multi-agent orchestrators, use `state.app` to pass custom data. See [Use hooks with multi-agent orchestrators](#use-hooks-with-multi-agent-orchestrators) below.
+
+
+
+
+## Use hooks with multi-agent orchestrators
+
+Register hooks on `Graph` or `Swarm` orchestrators for node-level events like `BeforeNodeCallEvent` and `AfterNodeCallEvent`.
+
+
+
+
+```python
+from strands import Agent
+from strands.hooks import BeforeNodeCallEvent, AfterNodeCallEvent
+from strands.multiagent import Graph
+
+researcher = Agent(system_prompt="You are a research specialist.")
+writer = Agent(system_prompt="You are a writing specialist.")
+
+graph = Graph(
+ agents={"researcher": researcher, "writer": writer},
+ edges=[("researcher", "writer")],
+)
+
+def log_node_start(event: BeforeNodeCallEvent) -> None:
+ print(f"Node {event.node_id} starting")
+
+def log_node_end(event: AfterNodeCallEvent) -> None:
+ print(f"Node {event.node_id} completed")
+
+graph.hooks.add_callback(BeforeNodeCallEvent, log_node_start)
+graph.hooks.add_callback(AfterNodeCallEvent, log_node_end)
+```
+
+
+
+
+```typescript
+import { Agent } from '@strands-agents/sdk'
+import { Graph, BeforeNodeCallEvent, AfterNodeCallEvent } from '@strands-agents/sdk/multiagent'
+
+const researcher = new Agent({ id: 'researcher', systemPrompt: 'You are a research specialist.' })
+const writer = new Agent({ id: 'writer', systemPrompt: 'You are a writing specialist.' })
+
+const graph = new Graph({
+ nodes: [researcher, writer],
+ edges: [['researcher', 'writer']],
+})
+
+graph.addHook(BeforeNodeCallEvent, (event) => {
+ console.log(`Node ${event.nodeId} starting`)
+})
+
+graph.addHook(AfterNodeCallEvent, (event) => {
+ console.log(`Node ${event.nodeId} completed`)
+})
+```
+
+
+
+
+### Layered hooks: agent-level and orchestrator-level
+
+Combine agent-level and orchestrator-level hooks. Both fire during execution:
+
+
+
+
+```python
+from strands import Agent
+from strands.plugins import Plugin, hook
+from strands.hooks import (
+ BeforeToolCallEvent,
+ BeforeNodeCallEvent,
+ AfterNodeCallEvent,
+ HookProvider,
+ HookRegistry,
+)
+from strands.multiagent import Graph
+
+# Agent-level plugin: logs every tool call inside each agent
+class AgentLoggingPlugin(Plugin):
+ name = "agent-logging"
+
+ @hook
+ def log_tool(self, event: BeforeToolCallEvent) -> None:
+ print(f" [agent] tool call: {event.tool_use['name']}")
+
+# Orchestrator-level hook provider: logs node transitions
+class OrchestratorLoggingHook(HookProvider):
+ def register_hooks(self, registry: HookRegistry) -> None:
+ registry.add_callback(BeforeNodeCallEvent, self.on_node_start)
+ registry.add_callback(AfterNodeCallEvent, self.on_node_end)
+
+ def on_node_start(self, event: BeforeNodeCallEvent) -> None:
+ print(f"[orchestrator] node {event.node_id} starting")
+
+ def on_node_end(self, event: AfterNodeCallEvent) -> None:
+ print(f"[orchestrator] node {event.node_id} completed")
+
+# Wire both layers together
+agent1 = Agent(system_prompt="You research topics.", plugins=[AgentLoggingPlugin()])
+agent2 = Agent(system_prompt="You write summaries.", plugins=[AgentLoggingPlugin()])
+
+graph = Graph(
+ agents={"researcher": agent1, "writer": agent2},
+ edges=[("researcher", "writer")],
+ hooks=[OrchestratorLoggingHook()],
+)
+```
+
+
+
+
+```typescript
+import { Agent } from '@strands-agents/sdk'
+import type { LocalAgent, Plugin } from '@strands-agents/sdk'
+import { BeforeToolCallEvent } from '@strands-agents/sdk'
+import {
+ Graph,
+ BeforeNodeCallEvent,
+ AfterNodeCallEvent,
+} from '@strands-agents/sdk/multiagent'
+import type { MultiAgent, MultiAgentPlugin } from '@strands-agents/sdk/multiagent'
+
+// Agent-level plugin: logs every tool call inside each agent
+class AgentLoggingPlugin implements Plugin {
+ name = 'agent-logging'
+
+ initAgent(agent: LocalAgent): void {
+ agent.addHook(BeforeToolCallEvent, (event) => {
+ console.log(` [agent] tool call: ${event.toolUse.name}`)
+ })
+ }
+}
+
+// Orchestrator-level plugin: logs node transitions
+class OrchestratorLoggingPlugin extends MultiAgentPlugin {
+ readonly name = 'orchestrator-logging'
+
+ initMultiAgent(orchestrator: MultiAgent): void {
+ orchestrator.addHook(BeforeNodeCallEvent, (event) => {
+ console.log(`[orchestrator] node ${event.nodeId} starting`)
+ })
+
+ orchestrator.addHook(AfterNodeCallEvent, (event) => {
+ console.log(`[orchestrator] node ${event.nodeId} completed`)
+ })
+ }
+}
+
+// Wire both layers together
+const agent1 = new Agent({ id: 'researcher', systemPrompt: 'You research topics.', plugins: [new AgentLoggingPlugin()] })
+const agent2 = new Agent({ id: 'writer', systemPrompt: 'You write summaries.', plugins: [new AgentLoggingPlugin()] })
+
+const graph = new Graph({
+ nodes: [agent1, agent2],
+ edges: [['researcher', 'writer']],
+ plugins: [new OrchestratorLoggingPlugin()],
+})
+```
+
+
+
+
+## Create a reusable hook collection (Python)
+
+Create a reusable hook collection by implementing the `HookProvider` protocol. This is lighter than a full plugin when you only need hooks:
+
+
+
+
+```python
+from strands import Agent
+from strands.hooks import (
+ HookProvider,
+ HookRegistry,
+ BeforeInvocationEvent,
+ AfterInvocationEvent,
+)
+
+class RequestLogger(HookProvider):
+ def register_hooks(self, registry: HookRegistry) -> None:
+ registry.add_callback(BeforeInvocationEvent, self.log_start)
+ registry.add_callback(AfterInvocationEvent, self.log_end)
+
+ def log_start(self, event: BeforeInvocationEvent) -> None:
+ print(f"Request started for agent: {event.agent.name}")
+
+ def log_end(self, event: AfterInvocationEvent) -> None:
+ print(f"Request completed for agent: {event.agent.name}")
+
+# Pass via hooks parameter
+agent = Agent(hooks=[RequestLogger()])
+
+# Or add after construction
+agent.hooks.add_hook(RequestLogger())
+```
+
+
+
+
+The TypeScript SDK does not export a `HookProvider` interface. Use [Plugin](../../plugins/index.md) to bundle multiple hooks together.
+
+
+