Best practices for the AWS Strands Agents SDK — structuring prompts, multi-agent patterns, structured I/O, and splitting monolithic agents into specialists. Use when designing or refactoring Strands-based agent systems.
Scanned 9/20/2026
Install to Claude Code
npx -y skills add tstapler/dotfiles --skill code-strands --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Code Strands?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/tstapler-code-strands)More formats (shields.io, HTML) on the badges page.
---
name: code-strands
description: Best practices for the AWS Strands Agents SDK — structuring prompts, multi-agent patterns, structured I/O, and splitting monolithic agents into specialists. Use when designing or refactoring Strands-based agent systems.
---
# Strands Agents SDK Best Practices
> For prompt design for Strands system prompts and tool descriptions, apply the `meta-prompt-engineering` skill.
## Core Philosophy
Strands is **model-driven**: agents decide what to do, tools define what's possible. Keep system prompts focused on a single domain of expertise. Fat prompts become brittle; specialists compose cleanly.
## `@tool` Decorator — How It Works
Strands builds the LLM tool spec from your function signature automatically:
```python
from strands import tool
@tool
def analyze_incident(incident_key: str, severity: str, days_back: int = 30) -> str:
"""Analyze a BTS incident and return classification recommendations.
Args:
incident_key: Jira ticket ID (e.g. BTS-12345)
severity: P1, P2, P3, or P4
days_back: Days back for comparison window
"""
...
```
- **First docstring paragraph** → tool description shown to the LLM (make it precise — this is the routing signal)
- **`Args:` section** → per-parameter descriptions in the tool spec
- **Type annotations** → JSON Schema types
- **Default values** → optional parameters
Override name/description or provide a full custom schema (e.g. for enums):
```python
@tool(name="get_weather", description="Retrieves weather forecast")
def weather_forecast(...): ...
@tool(inputSchema={"json": {"type": "object", "properties": {"shape": {"type": "string", "enum": ["circle", "rectangle"]}}, "required": ["shape"]}})
def calculate_area(shape: str): ...
```
## Agent-as-Tool Pattern (Primary Decomposition Strategy)
Wrap specialist agents in `@tool` functions. The orchestrator routes to them; each specialist has a short, focused system prompt.
```python
from strands import Agent, tool
@tool
def field_classification_specialist(context_json: str) -> str:
"""Assess missing classification fields and return a comment section if action needed."""
agent = Agent(
system_prompt=FIELD_CLASSIFICATION_PROMPT, # ~60 lines, single concern
tools=[get_transcript_field_suggestions, get_datadog_service_catalog, update_jira_field],
callback_handler=None, # suppress intermediate noise from appearing in orchestrator output
)
return str(agent(context_json))
# Orchestrator uses specialists as tools
orchestrator = Agent(
system_prompt=ORCHESTRATOR_PROMPT, # routing + combining logic only
tools=[field_classification_specialist, mitigated_closure_specialist, coe_specialist],
)
```
**Key rules:**
- `callback_handler=None` on sub-agents — prevents duplicate/noisy output in the orchestrator's stream
- Each specialist gets only the tools it needs; don't share tool lists
- The `@tool` docstring IS the routing description — make it unambiguous
> For typing inter-agent contracts with Pydantic models and parse-don't-validate, apply the `type-driven-design` skill.
## Structured Outputs Between Agents
Use Pydantic models for typed inter-agent contracts instead of string blobs:
```python
from pydantic import BaseModel
class WorkflowResult(BaseModel):
should_act: bool
comment_section: str | None # Markdown section for the combined comment
auto_updates: list[FieldUpdate] # Fields to apply before commenting
flag_for_human: str | None # Reason string if human review needed
# Agent produces structured output
result = agent("...", structured_output_model=WorkflowResult)
workflow_result: WorkflowResult = result.structured_output
```
**⚠ Known bug**: `structured_output_model` + `tools=` has known issues (GitHub #872, #891, #1032) where tool calls may not fire when structured output is active. A revamp is in progress. **Workaround**: use `structured_output_model` only on agents that don't need to call tools, or serialize the result as JSON string and deserialize on the receiving side.
**⚠ `str(AgentResult)` loses structured output**: when returning from an agent-as-tool, use `.model_dump_json()` to serialize the Pydantic model and parse it back on the orchestrator side:
```python
@tool
def my_specialist(query: str) -> str:
agent = Agent(system_prompt=PROMPT, tools=[...], callback_handler=None)
result = agent(query, structured_output_model=WorkflowResult)
return result.structured_output.model_dump_json() # serialize explicitly
# In orchestrator tool handler or post-processing:
workflow_result = WorkflowResult.model_validate_json(specialist_return_value)
```
## Passing Metadata Without Polluting LLM Context
Use `invocation_state` for configuration and metadata that tools need but the LLM shouldn't see in its token budget:
```python
result = orchestrator(message, invocation_state={
"issue_key": "BTS-12345",
"dry_run": True,
"jira_base_url": "https://betfanatics.atlassian.net/browse",
})
# Tools access it via ToolContext — never visible in the LLM prompt
@tool(context=True)
def add_jira_comment(body: str, tool_context: ToolContext) -> str:
issue_key = tool_context.invocation_state["issue_key"]
dry_run = tool_context.invocation_state.get("dry_run", False)
...
```
**⚠ `invocation_state` does NOT auto-propagate to sub-agents**: when a `@tool` spawns a sub-agent, the parent's `invocation_state` is not forwarded automatically. You must thread it explicitly:
```python
@tool(context=True)
def my_specialist(query: str, tool_context: ToolContext) -> str:
"""Run specialist agent."""
sub_agent = Agent(system_prompt=PROMPT, tools=[...], callback_handler=None)
# Manually forward needed state into the prompt or invocation_state
return str(sub_agent(query, invocation_state=tool_context.invocation_state))
```
## `Agent.__call__` Signature
```python
result: AgentResult = agent(
prompt, # str | list[ContentBlock] | list[Message] | None
invocation_state=None, # dict — context for tools, invisible to LLM
structured_output_model=None, # per-call override of agent-level default
)
# AgentResult fields:
result.stop_reason # why the agent stopped
result.message # final message
result.metrics # token counts, cycle durations, tool stats
result.structured_output # populated if structured_output_model was set
```
**Note**: `prompt` must be `str`, `ContentBlock` list, `Message` list, or `None` — not a raw dict or dataclass. Structured context must be serialized into the string or passed via `invocation_state`.
## Multi-Agent Patterns (When to Use Each)
| Pattern | Use When | How Context Flows |
|---------|----------|-------------------|
| **Agent-as-Tool** | Orchestrator delegates to specialists; results combine | Orchestrator collects returns, aggregates |
| **Graph** | Conditional routing with LLM-decided paths, cycles OK | Full conversation transcript shared across nodes |
| **Swarm** | Agents hand off to peers; exploration/multidisciplinary | Shared context with prior agent knowledge |
| **Workflow (DAG)** | Repeatable pipeline, parallel steps, deterministic | Task-specific context from dependencies only |
For structured processes with one combined output (e.g. incident management): **Agent-as-Tool** is correct — specialists are called by an orchestrator that owns the final assembly.
## Execution Patterns: Streaming, Conversation Management, Instance Lifecycle
Sub-agent event streaming (`stream_async`), the three built-in conversation-management strategies (sliding window, summarizing, manual), and the singleton-vs-fresh-instance tradeoff for agent-as-tool are covered in [Execution Patterns](references/execution-patterns.md).
**Defaults for stateless, parallelized incident processing** (our use case): fresh `Agent` instances per call, `SlidingWindowConversationManager` with `per_turn=True`.
> For optimizing context windows, compaction, and sub-agent isolation, apply the `meta-context-engineering` skill.
## Prompt Sizing Guidelines
No SDK-imposed limit — the constraint is the model's context window. Practical guidance:
- **Orchestrator**: routing logic + cross-cutting rules only (~50-80 lines / ~200-400 tokens)
- **Specialist**: one workflow domain only (~40-80 lines / ~100-500 tokens)
- **Rule of thumb**: if a prompt has two `---` section separators for unrelated concerns, it should be two agents
> For selecting which Claude model to use for orchestrators vs. specialists, apply the `meta-model-selection` skill.
## When to Split a Monolithic Agent
Split when **any** of these are true:
1. System prompt exceeds ~2,000 tokens with clearly distinct domain sections
2. Toolbelt has 15+ tools and wrong-tool selection is a recurring problem
3. Context window overflows regularly on complex runs
4. Some sub-tasks can run concurrently (use async)
5. Different domains warrant different model capabilities or costs
6. Multiple teams need to independently maintain different capabilities
**Model optimization** — the orchestrator only needs to route; use a cheap/fast model there. Specialist sub-agents can use more capable models where their domain requires it:
```python
orchestrator = Agent(
model=BedrockModel(model_id="amazon.nova-lite-v1:0"), # cheap router
tools=[field_classification_specialist, mitigated_closure_specialist],
)
# Each specialist uses its own model (defaulting to Sonnet)
```
## Splitting a Monolithic Prompt
1. Identify independent "workflows" or "concerns" in the prompt
2. Each concern becomes a specialist with its own system prompt + minimal tool set
3. Cross-cutting rules (comment formatting, unassigned handling, section ordering) stay in the orchestrator
4. Define a `WorkflowResult` Pydantic model as the contract; serialize with `.model_dump_json()` across the agent-as-tool boundary
5. Add `context=True` to specialist `@tool` functions so they can forward `invocation_state`
6. Orchestrator collects results, applies auto-updates, assembles and posts one combined output
## Known Limitations (as of 2026-02)
| Issue | Impact | Workaround |
|-------|--------|------------|
| `structured_output_model` + `tools` conflicts (GH #872, #891, #1032) | Tool calls may not fire when structured output active | Separate output-producing agents from tool-calling agents; serialize via JSON string |
| `invocation_state` not auto-propagated to sub-agents | Sub-agent tools can't see parent state | Pass `tool_context.invocation_state` explicitly to sub-agent `invocation_state=` |
| `str(AgentResult)` drops `structured_output` | Pydantic models lost across agent-as-tool boundary | Use `.model_dump_json()` / `model_validate_json()` explicitly |
| Structured output is Python-only | No TypeScript structured output | N/A |
---
## Related Skills
| Skill | When to apply |
|-------|--------------|
| `meta-prompt-engineering` | Crafting focused system prompts and tool descriptions for Strands agents |
| `meta-context-engineering` | Optimizing context windows, compaction, and sub-agent isolation |
| `meta-model-selection` | Choosing the right Claude model for orchestrators vs. specialists |
| `type-driven-design` | Typing inter-agent Pydantic contracts; parse-don't-validate at boundaries |
| `python-development` | Python standards (uv, Pydantic, async) for Strands tool implementations |
| `security-review` | Auditing tool permissions, invocation_state exposure, and agent trust boundaries |
## Reference
- [Execution Patterns](references/execution-patterns.md) — async streaming, conversation management, singleton vs fresh-instance
- [Agents as Tools](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/multi-agent/agents-as-tools/)
- [Multi-Agent Patterns](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/multi-agent/multi-agent-patterns/)
- [Structured Output](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/agents/structured-output/)
- [Custom Tools](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/tools/custom-tools/)
- [Callback Handlers / Streaming](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/streaming/callback-handlers/)
- [Agent API Reference](https://strandsagents.com/latest/documentation/docs/api-reference/python/agent/agent/)
- [strands-agents/samples](https://github.com/strands-agents/samples)
- [Deep Agents pattern (community)](https://www.pierreange.ai/blog/deep-agents-using-strands)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!