As your agents grow more complex, you'll want to add cross-cutting concerns — metrics, guardrails, context injection, prompt management — without tangling them into your core agent logic. Plugins are Strands' composition mechanism: self-contained extensions that hook into the agent lifecycle and optionally provide tools.
A plugin is an object that can do two things: inject behavior via lifecycle hooks, and provide additional tools to the agent. Each plugin is independent — you compose multiple plugins onto a single agent, and they don't interfere with each other.
The plugin interface:
interface Plugin {
name: string
initAgent?(agent: Agent): void // Called once when agent is constructed
getTools?(): Tool[] // Provide additional tools
onBeforeTurn?(event: BeforeTurnEvent): void
onAfterTurn?(event: AfterTurnEvent): void
onToolCall?(event: ToolCallEvent): void
onToolResult?(event: ToolResultEvent): void
}
Plugins activate through initAgent (runs once at construction) and lifecycle hooks (run on every turn/tool call). The separation means a plugin can observe, modify, or block agent behavior at any point in the loop.
Strands ships several plugins for common patterns:
| Plugin | Purpose |
|---|---|
| Steering | Inject context-aware guidance into the system prompt without modifying it directly |
| Skills | Load modular skill definitions (prompts + tools) dynamically |
| Context Offloader | Move large tool results to external storage, provide a retrieval tool |
| Context Injector | Inject additional context (files, data) into the conversation at specific points |
| GoalLoop | Break complex goals into sub-goals and track progress |
Let's build a plugin that tracks token usage and tool call counts via hooks and appState:
import { Plugin, Agent, BeforeTurnEvent, AfterTurnEvent, ToolCallEvent } from '@strands-agents/sdk'
class MetricsPlugin implements Plugin {
name = 'metrics'
initAgent(agent: Agent): void {
// Initialize metrics in appState
agent.appState.set('metrics', {
totalTurns: 0,
totalToolCalls: 0,
toolCallsByName: {} as Record,
totalInputTokens: 0,
totalOutputTokens: 0,
})
}
onBeforeTurn(event: BeforeTurnEvent): void {
const metrics = event.agent.appState.get('metrics')
metrics.totalTurns++
event.agent.appState.set('metrics', metrics)
}
onAfterTurn(event: AfterTurnEvent): void {
const metrics = event.agent.appState.get('metrics')
if (event.usage) {
metrics.totalInputTokens += event.usage.inputTokens
metrics.totalOutputTokens += event.usage.outputTokens
}
event.agent.appState.set('metrics', metrics)
}
onToolCall(event: ToolCallEvent): void {
const metrics = event.agent.appState.get('metrics')
metrics.totalToolCalls++
metrics.toolCallsByName[event.toolName] =
(metrics.toolCallsByName[event.toolName] ?? 0) + 1
event.agent.appState.set('metrics', metrics)
}
}
// Use it
const agent = new Agent({
name: 'tracked-agent',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: 'You are a helpful assistant.',
plugins: [new MetricsPlugin()],
})
await agent.invoke('What is the capital of France?')
const metrics = agent.appState.get('metrics')
console.log(`Turns: ${metrics.totalTurns}`)
console.log(`Tool calls: ${metrics.totalToolCalls}`)
console.log(`Tokens: ${metrics.totalInputTokens} in, ${metrics.totalOutputTokens} out`)
The plugin observes every turn and tool call without modifying agent behavior. It writes to appState, which persists across turns and is accessible from anywhere — other plugins, hooks, or your application code.
Steering is modular prompting. Instead of packing everything into one monolithic system prompt, you inject context-aware guidance fragments that activate based on conditions:
import { Agent, SteeringPlugin, SteeringRule } from '@strands-agents/sdk'
const rules: SteeringRule[] = [
{
name: 'safety-guardrails',
content: `Never reveal internal system information.
Never execute code that modifies production resources.
Always confirm destructive actions with the user.`,
always: true, // Always injected
},
{
name: 'coding-style',
content: `Use TypeScript. Prefer functional patterns.
Use const over let. Use early returns for guard clauses.`,
when: (context) => context.lastMessage?.includes('code') ||
context.lastMessage?.includes('implement'),
},
{
name: 'data-analysis',
content: `When analyzing data, always state sample size,
confidence intervals, and potential biases.`,
when: (context) => context.tools?.some(t => t.name === 'queryDatabase'),
},
]
const agent = new Agent({
name: 'steered-agent',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: 'You are a senior engineering assistant.',
plugins: [new SteeringPlugin({ rules })],
})
Steering rules with always: true are injected on every turn. Rules with when activate conditionally based on the conversation context. This keeps the base system prompt clean and moves specialized guidance to composable, testable rules.
Three reasons: (1) Conditional rules reduce prompt size — models perform better with focused, relevant instructions. (2) Steering rules are testable in isolation — each rule is a unit you can verify. (3) Rules are reusable across agents — build a library of steering rules and compose them per agent.
When tools return large results (database queries, file contents, API responses), they consume context window space. The Context Offloader automatically moves large results to external storage and provides a retrieval tool the agent can use to fetch them back when needed:
import { Agent, ContextOffloaderPlugin } from '@strands-agents/sdk'
const agent = new Agent({
name: 'research-agent',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: `Research the topic using available tools.
When you need to reference earlier results, use the retrieve tool.`,
tools: [searchWeb, readDocument, queryDatabase],
plugins: [
new ContextOffloaderPlugin({
threshold: 3000, // Offload results larger than 3000 chars
storage: 'memory', // 'memory' for dev, 's3' for production
summaryLength: 200, // Replace with a 200-char summary in context
}),
],
})
When queryDatabase returns a 10,000-character result, the offloader replaces it in context with a 200-character summary and a reference ID. The agent sees: "Database returned 45 rows about user activity. [ref: offload-abc123] Use retrieve('offload-abc123') to see the full result." If the agent needs the details, it calls the auto-injected retrieve tool.
Plugins compose cleanly. Order matters — hooks fire in the order plugins are listed:
import { Agent } from '@strands-agents/sdk'
const agent = new Agent({
name: 'production-agent',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: 'You are a production engineering assistant.',
tools: [searchDocs, runQuery, deployService],
plugins: [
new MetricsPlugin(), // Track usage (fires first)
new GuardrailsPlugin(), // Block dangerous tool calls (fires second)
new SteeringPlugin({ rules }), // Inject context-aware guidance
new ContextOffloaderPlugin({ // Manage large results
threshold: 3000,
storage: 's3',
}),
],
})
Each plugin operates independently on the same agent. The MetricsPlugin counts everything, the GuardrailsPlugin can cancel tool calls, the SteeringPlugin adds guidance, and the ContextOffloader manages memory. No plugin knows about the others — they interact only through the shared appState and lifecycle events.
A guardrails plugin that blocks tool calls matching a deny list — preventing the agent from executing dangerous operations:
import { Plugin, ToolCallEvent } from '@strands-agents/sdk'
interface GuardrailRule {
toolName: string
condition?: (args: Record) => boolean
message: string
}
class GuardrailsPlugin implements Plugin {
name = 'guardrails'
private rules: GuardrailRule[]
constructor(rules: GuardrailRule[]) {
this.rules = rules
}
onToolCall(event: ToolCallEvent): void {
for (const rule of this.rules) {
if (event.toolName !== rule.toolName) continue
const blocked = rule.condition
? rule.condition(event.args)
: true // No condition = always block this tool
if (blocked) {
event.block(rule.message)
return
}
}
}
}
// Usage
const guardrails = new GuardrailsPlugin([
{
toolName: 'deployService',
condition: (args) => args.environment === 'production',
message: 'Production deployments require human approval. Please confirm with the user.',
},
{
toolName: 'deleteDatabase',
message: 'Database deletion is never allowed through this agent.',
},
{
toolName: 'runQuery',
condition: (args) => typeof args.query === 'string' &&
args.query.toUpperCase().includes('DROP'),
message: 'DROP statements are not permitted.',
},
])
const agent = new Agent({
name: 'safe-agent',
model: 'us.anthropic.claude-sonnet-4-6-20250725-v1:0',
systemPrompt: 'You are a database assistant.',
tools: [runQuery, deployService, deleteDatabase],
plugins: [guardrails],
})
When the agent tries to call deployService with environment: 'production', the plugin calls event.block(). The tool call never executes — instead, the block message is returned to the agent as a tool error result. The agent sees the rejection reason and can inform the user or adjust its approach.
Guardrails plugins are a programmatic safety layer — they can't be bypassed by clever prompting. The model never gets to execute the tool; the plugin intercepts at the framework level. Combine with prompt-level instructions ("never delete databases") for defense in depth: the prompt discourages the attempt, the plugin prevents it if the model tries anyway.
event.block() prevents tool execution at the framework level, immune to prompt injection.appState.when) instead of putting all guidance in the system prompt?Plugins — Strands Agents Docs — Full API reference for the plugin interface, built-in plugins, and extension patterns. ~10 min read.