← All guides
Interactive guide Β· 10 levels

Your first agent with Strands + Python

From your first agent in 10 lines of code with OpenAI, to a production architecture with a multi-agent orchestrator and load balancing. With real code from the Strands Agents SDK (open source, AWS).

0 / 10 completed
Level 1 Β· Fundamentals

Your first agent

Goal: install the Strands Agents SDK, configure your OpenAI API key, and run your first agent in under 10 lines of code.

The Strands Agents SDK is an open source framework (built by AWS) for building AI agents with a "model-driven" approach: the model decides what to do at each step, and the SDK handles the execution loop (the agent loop) for you. It works with any provider β€” Bedrock, Anthropic, and also OpenAI directly.

1. Install the dependencies

TERMINAL
pip install strands-agents strands-agents-tools export OPENAI_API_KEY="sk-your-api-key-here"

2. Write your first agent

An agent in Strands needs just two things: a model and (optionally) tools. Here's what "Hello World" looks like:

agent.py
from strands import Agent from strands.models.openai import OpenAIModel model = OpenAIModel(model_id="gpt-4o") agent = Agent(model=model) response = agent("What is an AI agent, in one sentence?") print(response)

You run python agent.py and you already have a working conversational agent. No manual loops, no hand-rolled function-call parsing β€” the SDK handles the whole question β†’ reasoning β†’ answer cycle.

Challenge

Change the model_id to a different OpenAI model (a cheaper one, for example) and compare response speed. Then change the prompt to ask the agent to always answer in list format.

Level 2 Β· Personality

System prompt and the agent loop

Goal: understand the "agent loop" and control agent behavior with a system prompt.

Every time you talk to an agent, Strands runs the agent loop: it sends your message + history + available tools to the model, the model decides whether to answer directly or call a tool, and the cycle repeats until the model gives a final answer. The system_prompt is what shapes how the agent thinks on every turn of that cycle.

agent.py
from strands import Agent from strands.models.openai import OpenAIModel model = OpenAIModel( model_id="gpt-4o", params={"temperature": 0.3, "max_tokens": 400}, ) agent = Agent( model=model, system_prompt=( "You are a technical support assistant for a software company. " "Always answer in English, briefly and with numbered steps. " "If you don't know something, say so explicitly instead of making it up." ), ) print(agent("A customer says the app crashes when opening the camera."))

Parameters you'll use often

  • temperature β€” lower (0.0–0.3) for precise tasks/support, higher (0.7+) for creativity
  • max_tokens β€” response length limit, important for controlling cost
  • model_id β€” you can mix a cheap model for simple tasks with a more powerful one for complex reasoning
Challenge

Write a system prompt for an agent that only answers questions about a specific topic of your choice, and politely declines any other question.

Level 3 Β· Tools

Give your agent tools

Goal: create a custom tool with the @tool decorator and use prebuilt tools from the strands_tools package.

An agent without tools can only "talk." With tools, it can act: query an API, do a calculation, read a file. In Strands, any Python function becomes a tool with the @tool decorator β€” the type hints become the schema, and the docstring becomes the description the model uses to decide when to call it.

agent.py
from strands import Agent, tool from strands.models.openai import OpenAIModel from strands_tools import calculator @tool def current_weather(city: str) -> str: """Returns the currently reported weather for a given city. Args: city: name of the city, e.g. "Guatemala City" """ # Here you'd call a real weather API (OpenWeather, etc.) data = {"Guatemala City": "72Β°F, partly cloudy"} return data.get(city, "I don't have data for that city") model = OpenAIModel(model_id="gpt-4o") agent = Agent(model=model, tools=[current_weather, calculator]) print(agent("What's the weather in Guatemala City and what's 340*12?"))

The agent decides on its own, without you telling it explicitly, that it needs to call current_weather for the first part and calculator (a prebuilt tool from strands_tools) for the second β€” and combines both results into a single answer.

Challenge

Create a lookup_product(name: str) tool that returns price/stock from an in-memory dictionary, and verify the agent uses it correctly across several follow-up questions in a conversation.

Level 4 Β· Memory

Conversational memory and sessions

Goal: maintain context across multiple conversation turns and understand how Strands manages history.

An Agent object already keeps its conversation history as long as it lives in memory β€” every new call gets appended to the same thread. For long conversations, Strands offers conversation managers that decide how to truncate or summarize history so you don't exceed the model's context window.

agent.py
from strands import Agent from strands.models.openai import OpenAIModel from strands.agent.conversation_manager import SlidingWindowConversationManager model = OpenAIModel(model_id="gpt-4o") agent = Agent( model=model, system_prompt="You are a sales assistant for a tech store.", conversation_manager=SlidingWindowConversationManager(window_size=20), ) agent("Hi, I'm looking for a laptop for graphic design") agent("Which is the cheapest one you mentioned?") agent("Does that one have good battery life?") # The agent remembers the laptops mentioned earlier without you repeating them

The SlidingWindowConversationManager keeps the last N messages and automatically drops the oldest ones β€” useful for support or sales bots that run indefinitely without accumulating unbounded context (and unbounded cost).

Challenge

Simulate a 5-turn conversation and check what happens when you reduce window_size to a very small number (e.g. 2) β€” you'll notice the agent "forgets" the start of the chat.

Level 5 Β· Structured output

Typed responses with Pydantic

Goal: force the agent to return structured, validated data instead of free text.

When you're connecting the agent to another system (a database, a CRM, a frontend), you don't want to parse free text β€” you want validated JSON. Strands supports this natively using Pydantic models as the output schema.

agent.py
from pydantic import BaseModel, Field from strands import Agent from strands.models.openai import OpenAIModel class SupportTicket(BaseModel): category: str = Field(description="bug, question, or feature request") urgency: str = Field(description="low, medium, or high") summary: str = Field(description="one-sentence summary of the issue") model = OpenAIModel(model_id="gpt-4o") agent = Agent(model=model) result: SupportTicket = agent.structured_output( SupportTicket, "The app keeps crashing every time I upload a profile photo, it's urgent" ) print(result.category, result.urgency, result.summary) # -> "bug" "high" "The app crashes when uploading a profile photo"

This is the foundation for automating ticket classification, extracting data from documents, or any flow where the agent's output feeds code, not a human reading text.

Challenge

Define a Pydantic model to extract data from a product review (1-5 rating, sentiment, and whether it mentions a specific problem) and test it with 3 different example reviews.

Level 6 Β· Streaming and async

Real-time responses

Goal: use async invocation and handle agent events in real time, key for chat-style UIs.

For a production API or chat, you don't want to wait for the agent to finish "thinking" before showing something β€” you want to display tokens as they arrive. Strands exposes invoke_async() and a callback-handler system for this.

agent_async.py
import asyncio from strands import Agent from strands.models.openai import OpenAIModel model = OpenAIModel(model_id="gpt-4o") agent = Agent(model=model) async def main(): async for event in agent.stream_async("Explain what RAG is in 3 steps"): if "data" in event: print(event["data"], end="", flush=True) asyncio.run(main())

If you're building an API (FastAPI, for example) and don't need to see streaming in the terminal but only need to process events internally, you can disable live output with callback_handler=None when creating the agent, and handle the events yourself.

Challenge

Wrap the agent in a FastAPI endpoint that returns the response as a StreamingResponse, reusing the async generator above.

Level 7 Β· Multi-agent

Agents-as-Tools: your first team of agents

Goal: combine several specialized agents using the "agent as tool" pattern.

When a single agent starts taking on too many responsibilities (researching, writing, validating), it's worth splitting it into specialized agents. Strands' simplest pattern for this is Agents-as-Tools: you wrap an entire agent inside a @tool function, and another agent can "call" it just like any other tool.

agent_team.py
from strands import Agent, tool from strands.models.openai import OpenAIModel model = OpenAIModel(model_id="gpt-4o") researcher_agent = Agent( model=model, system_prompt="You research technical data and give factual, concise answers.", ) @tool def research(question: str) -> str: """Delegates a research question to the specialized researcher agent.""" return str(researcher_agent(question)) writer_agent = Agent( model=model, system_prompt=( "You are a technical blog writer. When you need factual data, " "use the 'research' tool before writing." ), tools=[research], ) print(writer_agent("Write a paragraph about why RAG reduces hallucinations"))

The writer agent decides on its own when to delegate to the researcher β€” you don't orchestrate the flow step by step, the model does. This pattern uses few extra tokens because the coordination itself is deterministic (a regular function call).

Challenge

Add a third "reviewer" agent that receives the writer's text and returns a corrected version, chaining all three roles together.

Level 8 Β· Orchestrator

Graph and Swarm: orchestration patterns

Goal: pick the right multi-agent pattern and build an orchestrator that routes tasks to specialists.

Strands ships three ways to coordinate multiple agents, each suited to a different case:

  • Agents-as-Tools (previous level) β€” simple, deterministic delegation, ideal when you already know which agent does what
  • Graph β€” a deterministic directed graph: agents are nodes, connections define the data flow between them. Ideal for predictable pipelines (e.g. research β†’ write β†’ review, always in that order)
  • Swarm β€” the agents dynamically decide among themselves who to hand the task to. More flexible, but uses more tokens because the model "reasons" about who to delegate to
orchestrator.py
from strands import Agent, tool from strands.models.openai import OpenAIModel model = OpenAIModel(model_id="gpt-4o") sales_agent = Agent(model=model, system_prompt="Specialist in sales and pricing questions.") support_agent = Agent(model=model, system_prompt="Specialist in technical support and bugs.") billing_agent = Agent(model=model, system_prompt="Specialist in billing and payments.") @tool def ask_sales(question: str) -> str: """Use for questions about pricing, products, or the purchase process.""" return str(sales_agent(question)) @tool def ask_support(question: str) -> str: """Use for questions about errors, bugs, or technical issues.""" return str(support_agent(question)) @tool def ask_billing(question: str) -> str: """Use for questions about invoices, payments, or refunds.""" return str(billing_agent(question)) orchestrator = Agent( model=model, system_prompt=( "You are the entry point for customer support. Analyze each message " "and ALWAYS delegate to the right specialist using the available tools. " "Do not answer domain questions directly yourself." ), tools=[ask_sales, ask_support, ask_billing], ) print(orchestrator("I got charged twice for my subscription this month")) # -> the orchestrator automatically routes this to ask_billing

This is the heart of a production agent architecture: an orchestrator (router) that never resolves the domain itself, it just decides who to delegate to β€” just like a human dispatcher in a call center.

Challenge

Add logging inside each ask_* tool to record how many times each specialist gets routed to β€” it's the first step toward real observability.

Level 9 Β· Production

Observability, errors, and guardrails

Goal: instrument the agent with traces, handle errors robustly, and set cost limits.

Before putting this into production, three things stop being optional: knowing what the agent did (traces), what happens when something fails (errors/retries), and how much it can end up costing (limits).

Observability with OpenTelemetry

Strands emits traces using the OpenTelemetry standard natively β€” every model call, every tool use, and every step of the event loop gets recorded, compatible with Jaeger, Grafana Tempo, AWS X-Ray, or Datadog.

TERMINAL
export STRANDS_OTEL_ENABLED=true export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" python orchestrator.py # Now every request is traced end-to-end in your observability backend

Error handling and retries

robust_agent.py
import time def call_with_retries(agent, message, attempts=3): for attempt in range(1, attempts + 1): try: return agent(message) except Exception as e: if attempt == attempts: raise wait = 2 ** attempt # exponential backoff print(f"Error ({e}), retrying in {wait}s...") time.sleep(wait)

Basic guardrails

  • max_tokens limit per response to avoid uncontrollably long answers
  • Require explicit confirmation for critical tools (e.g. ones that write to a database)
  • A maximum "steps" limit on the event loop, to prevent an agent from entering an infinite tool-calling cycle
Challenge

Add a counter for tokens used per session, and cut off the conversation (with a clear message to the user) if a budget you define is exceeded.

Level 10 Β· Final architecture

Orchestrator + load balancer in production

Goal: assemble everything above into a scalable architecture: agents as services, load-balanced, behind an orchestrator.

At this point you have an orchestrator that delegates to specialist agents β€” but with everything running in a single Python process, a traffic spike takes the whole thing down. The last step is splitting each agent into its own service, and spreading the load across multiple instances.

1. Each agent as its own service

You wrap each specialist agent in a FastAPI microservice, independently deployable (Docker, Lambda, Fargate, or EKS β€” Strands supports all four out of the box):

support_service.py
from fastapi import FastAPI from pydantic import BaseModel from strands import Agent from strands.models.openai import OpenAIModel app = FastAPI() model = OpenAIModel(model_id="gpt-4o") support_agent = Agent(model=model, system_prompt="Technical support specialist.") class Query(BaseModel): message: str @app.post("/ask") async def ask(q: Query): response = await support_agent.invoke_async(q.message) return {"response": str(response)} # uvicorn support_service:app --host 0.0.0.0 --port 8001

2. Multiple workers behind a load balancer

You run several replicas of each service (e.g. 3 instances of support_service) and put a load balancer (nginx, or a managed one like AWS ALB) in front, spreading traffic across them β€” so no specialist agent is a single point of failure:

nginx.conf
upstream support_agents { least_conn; # sends each request to the worker with fewest active connections server support-1:8001; server support-2:8001; server support-3:8001; } server { listen 80; location /support/ { proxy_pass http://support_agents/; } }

3. Job queue to decouple the orchestrator

For traffic spikes or long-running tasks, the orchestrator doesn't call the specialists directly β€” it queues the job (Redis, SQS) and workers consume it at their own pace. This keeps one slow agent from blocking everyone else:

final architecture
Client β”‚ β–Ό Orchestrator (Agent + routing tools) β”‚ β–Ό Job queue (Redis / SQS) β”‚ β”œβ”€β”€β–Ά Load Balancer ──▢ [Sales Worker x3] β”œβ”€β”€β–Ά Load Balancer ──▢ [Support Worker x3] └──▢ Load Balancer ──▢ [Billing Worker x3] β”‚ β–Ό OpenTelemetry (traces of the whole flow)

With this, you have the full path: from a one-line agent("hello") in Level 1, to a real production architecture with intelligent routing, horizontal scaling, and end-to-end observability.

Final challenge

Take the orchestrator from Level 8, split each specialist into its own FastAPI service, and set up 2 instances of one of them behind nginx with least_conn. That's your first multi-agent system in production.