Skip to content
Back to skills

Strands Best Practices

ASecurity

Best practices for the AWS Strands Agents SDK — structuring prompts,

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 20, 2026
documentationtypescriptpythonnodeawsrefactoringgitapidocumentation

Works with

  • api

Security analysis

A100/100

Scanned September 20, 2026

npx -y skills add tstapler/dotfiles --skill strands-best-practices --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Strands Best Practices?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Strands Best Practices
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tstapler-strands-best-practices/badge)](https://www.skillsdirectory.com/skills/tstapler-strands-best-practices)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: strands-best-practices
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

## 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

## 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.

## Monitoring Sub-Agent Tool Use (Async Streaming)

Bubble sub-agent events up through the tool layer using `stream_async`:

```python
@tool
async def my_specialist(query: str) -> AsyncIterator:
    """Run specialist agent and stream its progress."""
    agent = Agent(system_prompt=PROMPT, tools=[...], callback_handler=None)
    result = None
    async for event in agent.stream_async(query):
        yield event          # bubbles up tool_stream_event to parent callback
        if "result" in event:
            result = event["result"]
    yield str(result)
```

Graph/Swarm emit additional events: `multiagent_node_start`, `multiagent_node_stop`, `multiagent_handoff`, `multiagent_result`.

## Conversation Management (Context Window Control)

Three built-in strategies — pick based on session length and memory needs:

```python
from strands.agent.conversation_manager import (
    SlidingWindowConversationManager,
    SummarizingConversationManager,
)

# Default: drop oldest messages, truncate large tool results
agent = Agent(conversation_manager=SlidingWindowConversationManager(
    window_size=20,
    should_truncate_results=True,
    per_turn=True,   # proactive management before each model call
))

# Long-running agents that need early context: summarize instead of drop
agent = Agent(conversation_manager=SummarizingConversationManager(
    summary_ratio=0.3,
    preserve_recent_messages=10,
    summarization_agent=Agent(model=haiku_model),  # use a cheap model for summaries
))

# Manual control (no automatic truncation)
from strands.agent.conversation_manager import NullConversationManager
agent = Agent(conversation_manager=NullConversationManager())
```

For short-lived per-incident agents (our use case): `SlidingWindowConversationManager` with `per_turn=True` is appropriate — each agent run is bounded and fresh.

**Note**: Native token counting is not yet exposed (GitHub #1197); access `agent.messages` for manual inspection.

## Singleton vs Fresh Instance for Agent-as-Tool

```python
# Singleton: shared conversation history across all calls to this tool
_specialist = Agent(system_prompt="...", tools=[...])

@tool
def my_specialist(query: str) -> str:
    return str(_specialist(query))   # history accumulates — useful for stateful sessions


# Fresh instance: clean slate each call (our pattern for incident agents)
@tool
def my_specialist(query: str) -> str:
    agent = Agent(system_prompt="...", tools=[...], callback_handler=None)
    return str(agent(query))         # no cross-contamination between incidents
```

**For stateless, parallelized incident processing: always use fresh instances.**

## 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

## 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 |

## Reference

- [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)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…