Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Strands Best Practices

ASecurity

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

8 stars
0 votes
0 copies
0 views
Added 9/20/2026
documentationtypescriptpythonnodeawsrefactoringgitapidocumentation

Works with

api

Security Analysis

A100/100

Scanned 9/20/2026

Install to Claude Code

$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 automatically 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.

Download Zip
Files
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

tstaplertstapler
View sourceMore from tstapler →
SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

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 (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Related Skills

Context Fundamentals

Understand the components, mechanics, and constraints of context in agent systems. Use when designing agent architectures, debugging context-related failures, or optimizing context usage.

179001 votes

release-notes

Draft release notes and changelog entries from git history or merged PRs between two refs (tags/SHAs/branches), including breaking changes, migrations, and upgrade steps. Use when the user asks for release notes, changelog updates, or a GitHub Release draft.

1301 votes

docs-style-guide

Documentation style guide enforcer by @planetabhi. Applies and reviews the writing style guide when authoring or editing product documentation and tutorials. Use to check prose for voice, tense, word choice, inclusive language, formatting, code block, UI, Markdown, and number/date conventions.

11 votes

Caveman Help

Quick-reference card for all caveman modes, skills, and commands. One-shot display, not a persistent mode. Trigger: /caveman-help, "caveman help", "what caveman commands", "how do I use caveman".

1023330 votes

How It Works

Explain how claude-mem captures observations, when memory injection kicks in, and where data lives. Use when the user asks "how does claude-mem work?" or "what is this thing doing?".

942310 votes
View all in documentation →