2.0 From talking to doing
Agentic AI on AWS, hands-on labs by Shivank Agrawal

Giving agents
tools

Section 2. Your first agent could only talk. Here it gets hands: Python functions it can call, a ready-made toolbox, plain-English access to S3, DynamoDB and Lambda, and patterns for tools that stay fast in production.

Folder section-02-agent-tools Notebooks 01_building_tools 02_prebuilt_and_aws_tools 03_advanced_tool_patterns Model: Claude Sonnet 4.6 on Amazon Bedrock
Swipe sideways to see the full diagram
one function production-ready tools 1 @toola function becomesa tool 2 Many toolsthe agent picksthe order 3 Prebuilt toolboximport it,don't build it 4 use_awsAWS in plainEnglish 5 Custom toolsyour ownbusiness logic 6 Class and asyncshared state,parallel calls
The journey in this section. Each step reuses the one before it. By step 6 you know the three ways to write a tool, and when to reach for each.
2.0

From talking to doing

In Section 1 you built an agent that understands requests and writes clever answers. But it couldn't check the weather, read a file, query a database, or change anything in the real world.

That limit hurts the moment you try to build something practical. Picture an AI assistant for your team that discusses project requirements brilliantly, yet can't check whether the deployment pipeline is running, read the latest error logs, or update a ticket. Or a customer-service agent that understands tricky questions perfectly, but can't look up an order, process a refund or book an appointment.

Swipe sideways to see the full diagram
Team assistant Can talk about ✓ project requirements ✓ CI/CD and git concepts Can't do without tools ✗ check if the deployment pipeline is running ✗ read the latest error logs ✗ update a ticket's status Customer-service agent Can talk about ✓ complex customer questions ✓ the return policy, in friendly words Can't do without tools ✗ look up order information ✗ process a refund ✗ schedule an appointment
Without tools, even the best model is a very expensive chatbot. Everything in red needs the agent to reach outside itself: an API, a database, a file. That reach is what a tool gives it.
✦Think of it like this

A brilliant new hire on day one, before IT has given them any logins. They can discuss your product for hours, but they can't open Jira, can't read the dashboards, can't send a single email. Tools are those logins.

In this section we fix that by giving the agent : the ability to interact with real systems, fetch live data and do meaningful work beyond writing text. We start by turning plain Python functions into tools, then use ready-made tools that save days of work, then build custom tools for problems only your business has. By the end, your agents won't just talk about solutions; they'll carry them out.

What you'll learn here

Before you start

  1. 1

    A working Strands setup from Section 1

    Your virtual environment, AWS credentials and python setup/check_setup.py showing ✅ for both models.

  2. 2

    Basic Python

    Functions, return values, dictionaries and simple classes. We explain the new bits (decorators, type hints, async) as we go.

  3. 3

    AWS permissions for notebook 2

    Besides Bedrock, notebook 2 creates and deletes a small bucket, a table, a function and its role. An admin user in a sandbox account, or setup/iam-policy-course.json, covers all of it.

  4. 4

    A little and JSON (helpful, not required)

    Knowing that an is "send a request to a URL, get back" makes the web and AWS examples easier to follow.

NotebookWhat it coversWhere on this page
01_building_tools.ipynbFunction to tool with @tool, a tip calculator, a multi-tool sales assistant, a custom inventory checker2.1, 2.2, 2.4
02_prebuilt_and_aws_tools.ipynbPrebuilt tools (calculator, http_request, file_write) and use_aws against S3, DynamoDB and Lambda2.3
03_advanced_tool_patterns.ipynbThe database-connection problem and class-based tools; async tools that run in parallel2.5
terminal
cd section-02-agent-tools
pip install -r requirements.txt
jupyter lab

Every notebook opens with the same two cells: install the libraries, then choose the model once and keep it in a variable called model.

every notebook, setup cell
import os
from strands.models.bedrock import BedrockModel

MODEL_ID = os.getenv("BEDROCK_MODEL_ID", "global.anthropic.claude-sonnet-4-6")
REGION = os.getenv("AWS_REGION", os.getenv("AWS_DEFAULT_REGION", "us-east-1"))
os.environ.setdefault("AWS_REGION", REGION)

model = BedrockModel(model_id=MODEL_ID, region_name=REGION)
print(f"Using {MODEL_ID} in {REGION}")
iAlways pass the model

Every agent in this section is created with Agent(model=model, ...). If you leave model out, Strands falls back to its own built-in default, which can change between versions or may not be enabled in your account. Passing it explicitly keeps every lab on the same, known model.

!Two things you'll see while running

Clean-up: notebook 2 creates real AWS resources. Its last cell deletes them; always run it.

Deprecation notices: strands-agents-tools 0.8.x prints a warning for a few prebuilt tools (calculator, http_request, tavily, current_time). They still work; the course pins strands-agents-tools<0.9 so they keep working.

2.1

Why agents need tools

Suppose you've built an agent to help with your development workflow. It understands git, explains CI/CD and answers project questions. Then you actually use it.

You ask: "How many open issues do I have in my project repo, and which one has been sitting there the longest?" It replies: "I can't access your GitHub account, but you can check by going to your repository's Issues tab and sorting by creation date."

You already know how to check GitHub. That's why you asked. What you've built is a chatbot that can only answer generically from what it knows. Now give the same agent a GitHub tool and ask again:

  1. 1

    The agent works out that it needs GitHub's API

  2. 2

    It calls the GitHub tool

    list_issues(repo="myproject", state="open")

  3. 3

    Data comes back

    23 open issues; issue #47 "Memory leak in data processor" was created 127 days ago.

  4. 4

    The LLM answers with real facts

    "You've got 23 open issues. The oldest is #47 about a memory leak in the data processor; it's been open for 127 days. Probably worth tackling that one."

Swipe sideways to see the full diagram
Without tools How many open issues,and which is oldest? LLMno way into GitHub “I can't access your GitHub. Go to the Issuestab and sort by creation date.” With a GitHub tool How many open issues,and which is oldest? Agent+ LLM decides call list_issues(repo="myproject", state="open") GitHub API 23 open issues. Oldest: #47 “Memory leak indata processor”, open 127 days “You've got 23 open issues. The oldest is #47, a memory leak in the data processor, open for 127 days. Probably worth tackling that one.”
Tells you how vs does it for you. Without tools, an agent is an LLM chatbot that explains how you could find the answer. With tools, it finds the answer itself.
Try it

Same question, with and without the GitHub tool

  • YouHow many open issues do I have in my project repo, and which one has been sitting there the longest?
  • ModelNo tool can reach GitHub, so I can only explain how to look it up.
  • Agent“I can't access your GitHub account, but you can check by going to your repository's Issues tab and sorting by creation date.”

What LLMs can't do on their own

LLMs are remarkable at understanding and writing text: essays, explanations, code suggestions. But they have built-in limits, and each one maps to a kind of tool that fixes it.

LimitWhat it meansThe kind of tool that fixes it
Knowledge cutoffCan't know events or data after its last training updateSearch, news, weather and other live-data APIs
No real-world interactionCan't read files, call APIs or execute tasksFile, HTTP, email, database and AWS tools
No persistent memoryForgets past conversations once the session endsMemory tools and stores (Section 3)
No built-in verificationCan't fact-check or confirm its own output is rightCalculators, code execution, lookups against a source of truth

With tools, agents can query GitHub, read files and databases, check server status and more. A weather API tool fetches your city's weather, a news API tool gets the latest headlines, an email tool sends a message, a file-system tool reads and writes files, and a calculator tool does exact maths. You'll build or use several of these in this section.

Brain, coordinator, hands

Here is the relationship between an agent and its tools, in one picture.

Swipe sideways to see the full diagram
LLM Agent Tools the brain the coordinator the hands and senses understands requests,reasons, picks tools passes messages, runstools, keeps it all moving perform actions, gatherinformation from the world
LLM, agent and tools. The LLM never touches a tool directly. It asks; the agent runs the tool and brings the result back.
✦Think of it like this

A film shoot. The director (the LLM) decides each shot. The crew (the tools) work the cameras, lights and sound. The assistant director (the agent) runs the set: takes the director's call, gets the crew moving, and reports back what happened.

Kinds of tools to think about

When you design an agent, ask what it needs to find, do, compute, connect to, and any special media it handles. These five families cover almost everything.

Swipe sideways to see the full diagram
Information retrieval • Web search engines• Database queries• File reading• API clients for outside services• Document retrieval (RAG) Action execution • Email and messaging• Writing and editing files• Database updates• APIs that create or update things• Scheduling and automation Computation • Calculators and maths• Code execution sandboxes• Data analysis• Statistics Integration • AWS: S3, DynamoDB, Lambda• Third-party API connectors• Enterprise systems• Slack, Teams and other chat apps Specialised • Image generation and editing• Audio processing• Video analysis• ML model inference• Language processing
Five families of tools. A real agent usually mixes several: a support agent might retrieve (order lookup), compute (refund maths) and act (issue the refund).

From function to tool

Before building agents, let's turn one ordinary Python function into something an agent can use. Say you already have a function that checks whether your staging server is up:

01_building_tools.ipynb: a plain function
import requests


def check_server_status_plain(server_url):
    """Check if a server is responding."""
    try:
        response = requests.get(server_url, timeout=5)
        return f"Server is up. Status code: {response.status_code}"
    except requests.exceptions.RequestException:
        return "Server is down or unreachable"


print(check_server_status_plain("https://httpbin.org/get"))

It works when you call it. But ask an agent "Is the staging server running?" and it replies something like: "I don't have the ability to check server status. You can use curl or ping." The agent can't magically discover a function sitting in some Python file.

The fix is called , also known as tool use. To make the function available, add three things: the @tool (tells Strands to offer it to agents), (tell the agent what data to send) and a clear (tells it what the function does, so it knows when to use it).

01_building_tools.ipynb: the same function as a tool
from strands import Agent, tool


@tool  # Decorator
def check_server_status(server_url: str) -> str:  # Type hints
    """Check if a server is responding by making an HTTP request.

    Args:
        server_url: The URL of the server to check

    Returns:
        A message indicating whether the server is up or down
    """
    try:
        response = requests.get(server_url, timeout=5)
        return f"Server is up. Status code: {response.status_code}"
    except requests.exceptions.RequestException:
        return "Server is down or unreachable"
Swipe sideways to see the full diagram
@tool def check_server_status(server_url: str) -> str: """Check if a server is responding by making an HTTP request. Args: server_url: The URL of the server to check Returns: A message indicating whether the server is up or down """ try: response = requests.get(server_url, timeout=5) return f"Server is up. Status code: ..." except ...: return "Server is down or unreachable" @toolregisters the function as a tool Type hints → input schema“server_url” must be a string Docstring → descriptiontells the model what it does and when What the model sees name + description + input schema the body (try / except) is never shown to the model
Three ingredients, three jobs. The decorator makes it a tool, the type hints say what to send, the docstring says when to use it. The function body is private: the model only ever sees the spec and the result.

Inside, the function makes an HTTP GET request to the URL with a 5-second . If the server answers, it returns the (200 means success). If anything fails (server down, network error, timeout), it catches the and returns a friendly message instead of crashing. Now hand it to an agent:

01_building_tools.ipynb
agent = Agent(model=model, tools=[check_server_status])
response = agent("Is the staging server running? Check https://httpbin.org/get")

The agent now calls check_server_status with the right URL and answers from the real result. This pattern works for any function: add @tool, give it to the agent, and the agent can run it.

Try it

Remove an ingredient and see what the model loses

your function
@tool
def check_server_status(server_url: str) -> str:
    """Check if a server is responding by making an HTTP request.

    Args:
        server_url: The URL of the server to check
    """
    try:
        response = requests.get(server_url, timeout=5)
        return f"Server is up. Status code: {response.status_code}"
    except requests.exceptions.RequestException:
        return "Server is down or unreachable"

Roughly what the model is shown

{ "name": "check_server_status", "description": "Check if a server is responding by making an HTTP request.", "inputSchema": { "json": { "type": "object", "properties": { "server_url": { "type": "string", "description": "The URL of the server to check" } }, "required": [ "server_url" ] } } }

What that means

Ready. The model knows what the tool does, when to use it and exactly what to send.

When one tool isn't enough

Real requests usually need several steps. Imagine a sales assistant and the request: "Pull last quarter's sales data and email a summary to the team." That's not one task but three: query the database, analyse the numbers, send an email. So you build three small tools:

01_building_tools.ipynb: multi-tool sales assistant
@tool
def get_sales_data(quarter: str) -> dict:
    """Retrieve sales data for a specific quarter."""
    return {"revenue": 1250000, "deals": 47, "quarter": quarter}


@tool
def analyze_sales(revenue: int, deals: int, quarter: str) -> str:
    """Calculate key metrics from sales data."""
    avg_deal = revenue / deals
    return f"{quarter}: ${revenue:,} revenue, {deals} deals, ${avg_deal:,.0f} avg deal size"


@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email message."""
    print(f"\n[EMAIL] to={to} | subject={subject}\n{body}\n")
    return f"Email sent to {to}"


agent = Agent(model=model, tools=[get_sales_data, analyze_sales, send_email])
response = agent("Pull last quarter's sales data and email a summary to team@company.com")

Each tool does one job. The data tool only fetches data, the analysis tool only crunches numbers, the email tool only sends messages. That makes them reusable: analysing data from another source? Same analysis tool. Emailing something else? Same email tool. And you never wrote "if the user asks for a sales summary, do steps 1, 2, 3". The agent works out which tools to use, in what order, and how to pass data from one to the next.

Swipe sideways to see the full diagram
User Agent Model get_sales_data analyze_sales send_email “Pull last quarter's…” prompt + 3 tool specs use get_sales_data quarter="Q3 2026" {revenue: 1250000, deals: 47} here's the data use analyze_sales revenue=1250000, deals=47, quarter="Q3 2026" “Q3 2026: $1,250,000 revenue, 47 deals, $26,596 avg deal size” here's the summary use send_email to="team@company.com", subject="Q3 2026 sales summary", body=… “Email sent to team@company.com” “Done. Summary emailed.”
One request, three tool calls, nobody scripted the order. Each tool's output becomes the next tool's input, and the model is the one deciding that wiring at run time. (The quarter value is whatever the model infers "last quarter" means on the day you run it.)
Step through

Watch the sales request unfold

Press "Next step". The matching arrow in the diagram above lights up.
✓Tip: build focused, single-purpose tools

Resist one mega-tool that does everything. It's harder to maintain, harder for the agent to reason about, and impossible to reuse elsewhere. Think of a TV remote with 80 unlabelled buttons versus three clearly named ones: the model, like you, picks correctly when each button does one obvious thing.

Two more multi-tool chains

Swipe sideways to see the full diagram
“Find the GitHub repo for project X, read its README, and create a summary document” GitHub search repo URL file_read README text file_write “Get customer order #12345, calculate the refund with a 15% restocking fee, and process it” Order lookup order total Calculator refund amount Payment processing
Find, compute, act. Both chains follow the same shape: fetch something, work something out, then do something. The words on the arrows are the data the agent passes along.

This is what makes agents useful. You build focused tools, each doing one thing well. The agent figures out which to use, in what order, and how to pass data between them. You don't write workflows or state machines; you describe what each tool does and the agent handles the rest. Now let's build one end to end.

2.2

Your first tool-enabled agent

Let's build something you'd actually use: an agent that works out the tip and splits the bill when you're out for dinner with friends.

01_building_tools.ipynb: tip calculator
@tool
def calculate_tip(bill_amount: float, tip_percentage: float, num_people: int = 1) -> dict:
    """Calculate tip and split the bill among people.

    Args:
        bill_amount: Total bill amount in dollars
        tip_percentage: Tip percentage (e.g., 15, 18, 20)
        num_people: Number of people splitting the bill (default: 1)
    """
    tip = bill_amount * (tip_percentage / 100)
    total = bill_amount + tip
    per_person = total / num_people

    return {
        "bill": bill_amount,
        "tip": round(tip, 2),
        "total": round(total, 2),
        "per_person": round(per_person, 2),
    }


agent = Agent(model=model, tools=[calculate_tip])

response = agent(
    "The bill is $85. What's a 20% tip, and how much does each person pay if we're splitting it 4 ways?"
)
print("\n\nFinal answer:\n", response.message["content"][0]["text"])
  1. Step 1: @tool def calculate_tip(…) Create the tool: a normal Python function plus the three ingredients. num_people: int = 1 has a default, so it's optional.
  2. Args: section One line per parameter. This is how the model learns that "20%" means tip_percentage=20, not 0.2.
  3. return {…} A dictionary is fine. The agent hands the whole thing back to the model, which turns it into a sentence.
  4. Step 2: Agent(model=model, tools=[calculate_tip]) Give the tool to the agent.
  5. Step 3: agent("…") Ask in plain English. response.message["content"][0]["text"] is the text of the agent's final reply.
output (wording will vary)
I'll calculate the 20% tip and split the bill among 4 people for you.
Tool #1: calculate_tip
Here's the breakdown:
- **Bill amount**: $85.00
- **20% tip**: $17.00
- **Total with tip**: $102.00
- **Amount per person** (splitting 4 ways): $25.50

So each person should pay $25.50.

Final answer:
 Here's the breakdown: …  So each person should pay $25.50.
Splitting the bill. Strands streams the agent's words live, and prints a Tool #1: calculate_tip line the moment it runs your tool. The last block is our own print of the final answer.

So what just happened?

  1. 1

    Understood the intent

    You were asking about a tip calculation.

  2. 2

    Extracted the numbers

    $85, 20%, 4 people.

  3. 3

    Called calculate_tip with them

  4. 4

    Got the result back

  5. 5

    Wrote a natural, helpful reply

Swipe sideways to see the full diagram
“The bill is $85” “What's a 20% tip” “splitting it 4 ways” model extracts bill_amount = 85.0 tip_percentage = 20.0 num_people = 4 calculate_tip(…) {"bill": 85.0, "tip": 17.0, "total": 102.0, "per_person": 25.5}
From a sentence to function arguments. The orange arrows are the model's work: reading loose English and filling in typed parameters. The rest is just your Python running.

Here's what you didn't write:

  • Logic to parse "The bill is $85" and pull out the number
  • Code to recognise that "20%" means tip_percentage=20
  • Rules to work out that "splitting it 4 ways" means num_people=4
  • Any routing logic or if/else statements

The agent figured all of that out from what you described: the function name, its parameters and its docstring.

Try it yourself

Ask the same agent differently phrased questions. It understands intent, not just keywords:

01_building_tools.ipynb
agent("What's a 15% tip on $42?")
agent("Bill is $120, we want to tip 18%, split between 3 people")
agent("Calculate tip for $67.50 at 20%")
Try it

Tip agent playground

Or type your own

Tool call the model would make

{ "name": "calculate_tip", "input": { "bill_amount": 85, "tip_percentage": 20, "num_people": 4 } }

Tool result (computed for real)

{ "bill": 85, "tip": 17, "total": 102, "per_person": 25.5 }
Agent: A 20% tip on $85.00 is $17.00, making the total $102.00. Split 4 ways, each person pays $25.50.

In the real agent the LLM does the extraction. Here a small pattern matcher stands in for it, so very unusual phrasings may trip it up; the model would cope.

The whole recipe, again

  1. 1

    Create a tool

    A Python function with @tool.

  2. 2

    Give it to an agent

    Add it to the tools list.

  3. 3

    Let the model decide when to use it

Whether the tool calculates tips or checks servers, the approach is identical; only the inside of the function changes. The agent needs to understand just three things about any tool, and it learns all three from your function's name, parameters and docstring:

Swipe sideways to see the full diagram
What does it do? from the name and docstring calculate_tip “Calculate tip and splitthe bill among people.” What does it need? from parameters and Args bill_amount: float tip_percentage: float num_people: int = 1 What does it give back? from the return type and value -> dict bill, tip, total,per_person
What, needs, returns. Write these three clearly and the agent will call your tool at the right moments with the right arguments.
✦Think of it like this

Splitting the bill at a dhaba: one friend says "₹1,700, add 10% for the waiter, five of us", and another opens the calculator app. The friend talking is the model (understands loose words), the calculator app is your tool (exact, boring, reliable), and whoever reads the result aloud is the agent.

2.3

The Strands tool ecosystem

You now know how to turn a Python function into a tool. The good news: you don't have to build every tool yourself.

When you start a software project, you don't write your own HTTP library or JSON parser; you install packages others built. Agent tools work the same way. Why spend three days building a web scraper when someone has already solved it? The agents in later sections reuse these same tools, just combined in more interesting ways.

✦Think of it like this

Your phone's app store. You don't build a maps app or a UPI app before you can get anything done; you install them and get on with your day. The Strands toolbox is an app store for agent abilities: install once, then just import the tool you need.

Strands gives you tools from four places, and the agent treats them all the same way:

Swipe sideways to see the full diagram
Community toolsstrands-agents-tools calculatorfile_readhttp_request… dozens more Custom Python toolsyour @tool functions your internal APIsyour databaseyour business logic MCP toolsplug in any MCP server AWS docs serverGitHub serveranyone's MCP server AWS integrationsyour cloud, as tools use_awsS3, DynamoDB, LambdaSageMaker, Bedrock, … Agent(model=model, tools=[ … all of them … ]) The agent sees one catalog. It doesn't care where a tool came from.
Four pillars, one tools list. Start with built-in tools for common jobs, add custom tools for your business logic, and plug in MCP tools for specialised services.
PillarWhat it gives youWhere you use it
Prebuilt community toolsThe strands-agents-tools package: dozens of ready tools for files, shell commands, web search, HTTP requests, maths, AWS and more. Think of it as a package manager for agent abilities.This section
Custom Python toolsWhen nothing prebuilt fits, such as your company's internal API or domain-specific logic, @tool turns any function into a tool.2.1, 2.2, 2.4, 2.5
toolsA whole ecosystem of standardised tools that work across agent frameworks. If someone has built an MCP server for a service (a database connector, an API wrapper), you plug it in without writing integration code. Public catalogues of MCP servers make them easy to find.Section 5
AWS service integrationsAWS-specific tools in the package (like use_aws), plus custom tools you write for S3, DynamoDB, Lambda, SageMaker, Bedrock and other services. Your agent gets direct access to your cloud infrastructure and data.This section

Prebuilt community tools

These tools handle common jobs out of the box: file operations, web searches, HTTP calls, AWS services, maths, even multi-agent orchestration. They're already installed with this section's requirements.txt; on their own you'd install them with:

terminal
pip install strands-agents-tools

Here's the prebuilt calculator in action. One import gives the agent expressions, equation solving and calculus:

02_prebuilt_and_aws_tools.ipynb: calculator tool
from strands import Agent
from strands_tools import calculator

agent = Agent(model=model, tools=[calculator], system_prompt="You are a helpful math assistant.")

agent("What's 42 raised to the power of 9?")
agent("Solve the equation x^2 + 5x + 6 = 0")
agent("What's the derivative of sin(x) * cos(x)?")
QuestionWhat the calculator returns
42 to the power of 9406,671,383,849,472 (exact; an LLM guessing "in its head" often gets digits wrong)
x² + 5x + 6 = 0x = −2 or x = −3
d/dx of sin(x)·cos(x)cos²(x) − sin²(x), which simplifies to cos(2x)

Under the hood the calculator uses , a symbolic maths library, so it handles complex expressions and calculus that would take real effort to implement yourself.

What's in the toolbox?

Think of these as categories of abilities you can add to an agent instantly:

CategoryToolsWhat they do
File operationsfile_read, file_write, editorRead and write files; advanced editing with syntax highlighting
MathscalculatorCalculations, symbolic maths, equation solving
Web and searchhttp_request, tavily_search, tavily_extract, exa_search, bright_dataAPI calls, live web search, extracting page content, web scraping
Code executionpython_repl, shell, code_interpreterRun Python, run shell commands, isolated sandbox execution
AWS servicesuse_aws, retrieve, nova_reels, agent_core_memoryWork with S3, DynamoDB, Lambda; query Bedrock Knowledge Bases; create videos; store memories
Memory and storagememory, mem0_memory, mongodb_memory, elasticsearch_memoryStore and retrieve documents and memories across runs
Communicationslack, speakPost to Slack workspaces; text-to-speech output
Image and videogenerate_image, image_reader, search_video, chat_videoCreate and analyse images; semantic video search
Automation and utilitiesbrowser, use_computer, cron, current_time, sleep, diagram, rssWeb automation, desktop control, scheduling, time utilities, diagramming
Advancedswarm, workflow, use_llm, batch, mcp_clientMulti-agent coordination, automated workflows, nested AI loops, parallel execution
iAbout those deprecation warnings

In strands-agents-tools 0.8.x, calculator, http_request, tavily and current_time print a deprecation notice. They still work, and the course pins the version below 0.9 so they keep working. When you move on, their replacements are http_request/web_fetch from strands.vended_tools, the official Tavily MCP server, and ContextInjector for the current time.

Try it

Pick the right tools for the job

Toolbox: tap the tools this agent needs

Pick a task first.

Many tools, one request

Give the agent several prebuilt tools and it works out how to use them together. Here it fetches a live weather forecast for Bangalore, crunches the numbers and saves a summary:

02_prebuilt_and_aws_tools.ipynb: combining tools
from strands_tools import file_write, http_request

os.environ["BYPASS_TOOL_CONSENT"] = "true"

agent = Agent(
    model=model,
    tools=[http_request, calculator, file_write],
    system_prompt="You help with data analysis tasks.",
)

agent("""
Fetch the 7-day forecast from
https://api.open-meteo.com/v1/forecast?latitude=12.97&longitude=77.59&daily=temperature_2m_max,temperature_2m_min&timezone=Asia/Kolkata
extract the daily maximum temperatures for Bangalore,
calculate the average maximum temperature over the period,
and save a short summary to weather_summary.txt
""")
02_prebuilt_and_aws_tools.ipynb: check the file
if os.path.exists("weather_summary.txt"):
    print(open("weather_summary.txt").read())
else:
    print("No weather_summary.txt yet — scroll up to see which tools the agent called.")
Swipe sideways to see the full diagram
http_requestGET the Open-Meteo URLfor Bangalore calculatoraverage of the 7 dailymaximum temperatures file_writesave a short summary toweather_summary.txt "temperature_2m_max": [29.1, 28.4, 27.9, …]JSON from the API (example values) (29.1 + 28.4 + … ) / 7the model writes the expression,the tool computes it exactly “Bangalore, next 7 days:average max ≈ 28 °C …”example summary
Fetch, compute, save. You gave one paragraph of English. The agent chose the order (http_request, then calculator, then file_write) and passed each result to the next tool.
AaNew word: tool consent

Tools that change things on your machine, like file_write, normally stop and ask you to confirm first. Setting the BYPASS_TOOL_CONSENT=true skips that prompt so the demo runs end to end. It's like turning off the OTP step for payments: fine in a sandbox, never in a real application.

AWS integration with use_aws

Most real applications run in the cloud. Your data sits in S3 buckets, your records in DynamoDB or RDS, your business logic in Lambda functions. An agent that can't touch these is disconnected from your actual systems. The prebuilt use_aws tool fixes that: describe what you want in English, and the agent turns it into AWS API calls, made with .

Swipe sideways to see the full diagram
“List all S3 bucketsin my account” Agent+ model use_aws call the model writes service_name="s3" operation_name="list_buckets" parameters={} region="us-east-1" label="List S3 buckets" Create, update or delete? asks you to confirm first (unless BYPASS_TOOL_CONSENT) boto3 call with your credentials IAM checkallowed for you? AWS services S3DynamoDBLambda, and more result back to the agent
One tool, every AWS service. The model picks the service, operation and parameters; use_aws makes the boto3 call. Two safety gates stay in place: a confirmation for changes, and your IAM permissions.
02_prebuilt_and_aws_tools.ipynb: 3.1 list your S3 buckets
from strands_tools import use_aws

aws_agent = Agent(
    model=model,
    tools=[use_aws, calculator],
    system_prompt=f"You are an AWS assistant that helps manage cloud resources. Use region {REGION}.",
)

aws_agent("List all S3 buckets in my account")
✦Think of it like this

use_aws is a helper at a government office who knows every form by heart. You say "I want to renew my licence", and they fill in the right form for the right counter. But your ID card still decides which counters will serve you. That ID card is IAM: use_aws can only do what your credentials are allowed to do.

Three real scenarios, on resources the notebook creates for you

The scenarios need real resources to act on, so notebook 2 first creates small demo ones, each with a random suffix so nothing you already own is touched, and deletes them all at the end.

Swipe sideways to see the full diagram
3.0 Create (plain boto3) S3 bucket + sales-2024.csv DynamoDB customers-<suffix>with customer 12345 Lambda order-processor-<suffix>+ an IAM role for it 3.1 to 3.4 The agent works list buckets read CSV, total, write summary look up and update a customer invoke the Lambda function 3.5 Clean up delete objects + bucket delete the table delete the function detach and delete the role always run this cell
Create, use, delete. Setup and clean-up use plain boto3 so they're predictable; only the middle part is the agent working in English.
Show the setup cell (3.0) that creates the demo resources
02_prebuilt_and_aws_tools.ipynb: 3.0 create the demo resources
import csv
import io
import json
import time
import zipfile

import boto3

session = boto3.Session(region_name=REGION)
account_id = session.client("sts").get_caller_identity()["Account"]
s3 = session.client("s3")
dynamodb = session.client("dynamodb")
iam = session.client("iam")
lambda_client = session.client("lambda")

import uuid

suffix = uuid.uuid4().hex[:6]  # unique names, so we never touch resources you already have
BUCKET = f"agent-tools-demo-{account_id}-{REGION}-{suffix}"
TABLE = f"customers-{suffix}"
FUNCTION = f"order-processor-{suffix}"
ROLE_NAME = f"order-processor-demo-role-{suffix}"

# --- S3 bucket + CSV -------------------------------------------------------
if REGION == "us-east-1":
    s3.create_bucket(Bucket=BUCKET)
else:
    s3.create_bucket(Bucket=BUCKET, CreateBucketConfiguration={"LocationConstraint": REGION})

rows = [
    ("month", "region", "revenue"),
    ("2024-01", "North", 120000), ("2024-02", "North", 135000), ("2024-03", "North", 142000),
    ("2024-01", "South", 98000), ("2024-02", "South", 104000), ("2024-03", "South", 111000),
]
buf = io.StringIO()
csv.writer(buf).writerows(rows)
s3.put_object(Bucket=BUCKET, Key="sales-2024.csv", Body=buf.getvalue().encode())
print(f"S3 bucket ready: s3://{BUCKET}/sales-2024.csv")

# --- DynamoDB table + one customer -----------------------------------------
dynamodb.create_table(
    TableName=TABLE,
    KeySchema=[{"AttributeName": "customer_id", "KeyType": "HASH"}],
    AttributeDefinitions=[{"AttributeName": "customer_id", "AttributeType": "S"}],
    BillingMode="PAY_PER_REQUEST",
)
dynamodb.get_waiter("table_exists").wait(TableName=TABLE)
dynamodb.put_item(
    TableName=TABLE,
    Item={"customer_id": {"S": "12345"}, "name": {"S": "Asha Rao"}, "email": {"S": "asha@example.com"}},
)
print(f"DynamoDB table ready: {TABLE}")

# --- Lambda function ---------------------------------------------------------
trust = {
    "Version": "2012-10-17",
    "Statement": [{"Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}],
}
role_arn = iam.create_role(RoleName=ROLE_NAME, AssumeRolePolicyDocument=json.dumps(trust))["Role"]["Arn"]
iam.attach_role_policy(RoleName=ROLE_NAME, PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole")
time.sleep(10)  # let IAM propagate

handler_code = '''
import json

def handler(event, context):
    order_id = event.get("order_id", "unknown")
    return {"order_id": order_id, "status": "PROCESSED", "message": f"Order {order_id} processed successfully"}
'''
zip_buf = io.BytesIO()
with zipfile.ZipFile(zip_buf, "w") as z:
    z.writestr("lambda_function.py", handler_code)

lambda_client.create_function(
    FunctionName=FUNCTION,
    Runtime="python3.12",
    Role=role_arn,
    Handler="lambda_function.handler",
    Code={"ZipFile": zip_buf.getvalue()},
    Timeout=10,
)
lambda_client.get_waiter("function_active_v2").wait(FunctionName=FUNCTION)
print(f"Lambda function ready: {FUNCTION}")

Data analysis on S3

Read a from S3, total the revenue, and write a summary back.

02_prebuilt_and_aws_tools.ipynb: 3.2
aws_agent(
    f"Read the sales data from S3 bucket '{BUCKET}' key 'sales-2024.csv', "
    "calculate the total revenue, and save a short summary back to the same bucket as 'sales-summary.txt'"
)

The six rows add up to 710,000 (North 397,000 + South 313,000). Expect the agent to read the file with S3 get_object, total it with the calculator, then write sales-summary.txt with put_object.

Customer service on DynamoDB

02_prebuilt_and_aws_tools.ipynb: 3.3
aws_agent(
    f"Look up customer ID 12345 in the DynamoDB table '{TABLE}' and update their email to newemail@example.com. "
    "Then read the item back to confirm."
)

Workflow orchestration with Lambda

02_prebuilt_and_aws_tools.ipynb: 3.4
aws_agent(f"Invoke the Lambda function '{FUNCTION}' with the payload {{\"order_id\": \"67890\"}} and tell me the result")
Try it

See the AWS calls behind each English request

  • YouList all S3 buckets in my account
  • use_awss3.list_buckets()
  • Result3 buckets, including agent-tools-demo-…-a1b2c3
  • Agent“You have 3 S3 buckets: … and agent-tools-demo-…-a1b2c3.”

Look at those requests again. You described what you wanted (read this file, update this record, invoke this function) and the agent worked out the AWS API calls. You didn't write boto3 code, handle AWS responses, or even name the operation. The pattern is: one tool (use_aws), many services, plain-English requests.

!The resources must exist, and must be cleaned up

These requests only work because cell 3.0 created the bucket, CSV, table and function first. Run the clean-up cell when you're done:

02_prebuilt_and_aws_tools.ipynb: 3.5 clean up
# S3: delete all objects, then the bucket
for obj in s3.list_objects_v2(Bucket=BUCKET).get("Contents", []):
    s3.delete_object(Bucket=BUCKET, Key=obj["Key"])
s3.delete_bucket(Bucket=BUCKET)
print("Deleted bucket", BUCKET)

dynamodb.delete_table(TableName=TABLE)
print("Deleted table", TABLE)

lambda_client.delete_function(FunctionName=FUNCTION)
iam.detach_role_policy(RoleName=ROLE_NAME, PolicyArn="arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole")
iam.delete_role(RoleName=ROLE_NAME)
print("Deleted Lambda function and role")

You've now used community tools and AWS integrations. Next, the tools only you can build.

2.4

Building custom tools

When no community tool fits, you build your own: an internal API only your company uses, a proprietary database with its own business rules, or something nobody has built yet.

Say you run an online store, and customers ask "Is product X in stock?" all day. You need a tool that checks your inventory:

01_building_tools.ipynb: inventory checker
@tool
def check_inventory(product_id: str) -> str:
    """Check if a product is in stock.

    Args:
        product_id: The product ID to check (e.g., "PROD-123")
    """
    inventory = {"PROD-123": 15, "PROD-456": 0, "PROD-789": 8}
    quantity = inventory.get(product_id, 0)

    if quantity > 0:
        return f"Product {product_id} is in stock. We have {quantity} units available."
    return f"Product {product_id} is currently out of stock."


agent = Agent(model=model, tools=[check_inventory])

# All of these work — the agent understands intent, not just keywords
agent("Is PROD-123 in stock?")
agent("Do we have PROD-456 available?")
agent("Check inventory for PROD-789")
agent("Can I order PROD-123 right now?")
  1. @tool Without it, this is just regular Python the agent never sees.
  2. The docstring When someone asks "Is PROD-123 in stock?", the model reads "Check if a product is in stock" and recognises this tool fits.
  3. Args: product_id … (e.g., "PROD-123") Tells the model which piece of the question to extract, and what it looks like.
  4. inventory = {…} A dictionary standing in for a database. In production you'd replace it with a real query or an API call to your inventory system; nothing else changes.
  5. return … A plain sentence the model can use directly in its reply.

Four differently worded questions, and the agent recognises all of them as inventory checks. In traditional chatbot development you would write patterns or train an to handle those variations, map "in stock", "available" and "can I order" to the same intent, and extract the product ID from different sentence shapes. Here you skipped all of that. You wrote one function with a clear description, and the language model handles intent recognition and parameter extraction.

Swipe sideways to see the full diagram
Traditional chatbot: you build every box “Is PROD-123 in stock?”“Do we have PROD-456…”“Check inventory for…”“Can I order PROD-123…” Intent classifiertrained on many examples Regex for the IDPROD-\d+ Phrase → intent map “in stock” → CHECK_INV“available” → CHECK_INV“can I order” → CHECK_INV… and every new phrasing handler code Agent: you write one function with a good docstring any of the four phrasings,or a fifth you neverthought of Model reads the docstring “Check if a product is in stock”and pulls out the product ID check_inventory("PROD-123")
Four red boxes you no longer build. Intent recognition and parameter extraction move into the model. Your job shrinks to writing a clear function and docstring.
Try it

Regex chatbot vs agent: who understands the question?

Or type your own

Regex chatbot (matches only "in stock" or "available")

Matched. Product PROD-123 is in stock. We have 15 units available.

Agent with check_inventory

check_inventory(product_id="PROD-123")
Agent: “Product PROD-123 is in stock. We have 15 units available.”

The agent column runs the real tool logic (15, 0 and 8 units). A small matcher stands in for the model's understanding, so treat it as a demo of the idea.

Your turn: build one tool you care about

The best way to make this stick is to build a tool for a problem you actually have. Pick one, write it, give it to an agent, and ask questions in your own words. Rough logic is fine; what matters is seeing your tool fit into the agent loop.

IdeaA signature to start fromAsk it
Weather in your cityget_weather(city: str) -> str (call a weather API)"Do I need an umbrella in Pune today?"
Validate an emailis_valid_email(email: str) -> bool"Is asha@example a valid address?"
Age from birthdateage_from_birthdate(birthdate: str) -> int"How old is someone born on 1990-08-15?"
BMIcalculate_bmi(height_cm: float, weight_kg: float) -> dict"I'm 172 cm and 70 kg, what's my BMI?"
Shipping costshipping_cost(weight_kg: float, distance_km: float) -> float"Cost to ship 3 kg from Bangalore to Delhi?"
iYou already know enough for most tools

Plain @tool functions cover the majority of what you'll build. As your agents grow, though, you'll hit situations where they aren't enough: several tools sharing an expensive resource, slow APIs, and inputs that need strict validation. You don't need to memorise these now; just know they exist so you recognise them when they show up.

2.5

When simple tools aren't enough

Your inventory agent now has five tools: check stock, update stock, get supplier info, track shipments and generate reports. You deploy it. Within an hour your database administrator messages you: "Why is your agent opening 50 database connections per minute?"

You check the logs: every tool opens its own database connection. One user request may trigger three or four tool calls, so three or four connections, and dozens of users at once means the database is drowning. The @tool decorator is perfect for simple cases, but as agents grow you need more control. Notebook 3 walks through two such situations with real, countable numbers, using a small local SQLite database.

The database connection problem, and class-based tools

First the notebook creates a tiny inventory database and a connect_to_database() helper that counts every connection it opens:

03_advanced_tool_patterns.ipynb: setup
import sqlite3

DB_PATH = "inventory_demo.db"

# Create and seed a small inventory database
with sqlite3.connect(DB_PATH) as conn:
    conn.execute("DROP TABLE IF EXISTS inventory")
    conn.execute("CREATE TABLE inventory (id TEXT PRIMARY KEY, name TEXT, quantity INTEGER, price REAL)")
    conn.executemany(
        "INSERT INTO inventory VALUES (?, ?, ?, ?)",
        [
            ("PROD-123", "Wireless Mouse", 15, 29.99),
            ("PROD-456", "USB-C Hub", 0, 49.99),
            ("PROD-789", "Mechanical Keyboard", 8, 89.99),
        ],
    )
print("Database ready:", DB_PATH)

CONNECTIONS_OPENED = 0


def connect_to_database():
    """Open a new database connection and count it."""
    global CONNECTIONS_OPENED
    CONNECTIONS_OPENED += 1
    # check_same_thread=False: the agent may run tools on worker threads
    return sqlite3.connect(DB_PATH, check_same_thread=False)

Now the naive version, where each tool opens a connection, uses it once and closes it:

03_advanced_tool_patterns.ipynb: one connection per tool call
from strands import Agent, tool


@tool
def check_stock(product_id: str) -> str:
    """Check product stock."""
    db = connect_to_database()
    row = db.execute("SELECT quantity FROM inventory WHERE id = ?", (product_id,)).fetchone()
    db.close()
    return f"{product_id}: {row[0]} units" if row else f"{product_id} not found"


@tool
def update_stock(product_id: str, quantity: int) -> str:
    """Update product stock."""
    db = connect_to_database()
    db.execute("UPDATE inventory SET quantity = ? WHERE id = ?", (quantity, product_id))
    db.commit()
    db.close()
    return f"Updated {product_id} to {quantity} units"


CONNECTIONS_OPENED = 0
agent = Agent(model=model, tools=[check_stock, update_stock])
agent("Check stock for PROD-123 and PROD-789, set PROD-456 to 25 units, then confirm PROD-456's new level")

print(f"\n\nConnections opened for one request: {CONNECTIONS_OPENED}")

That one request needs four tool calls (two checks, an update, a confirm), so it prints about 4 connections. Harmless here, painful in production, because opening a connection is expensive:

  • It takes time. Each new connection needs a handshake, typically 50 to 100 milliseconds over a network, before any query runs.
  • It uses server memory for every open connection.
  • Databases cap them. By default MySQL allows 151 simultaneous connections and PostgreSQL 100. Hit the cap and your agent starts failing with "too many connections".

What you want is to connect once when the agent starts and share that connection across all the tools. Separate decorated functions have no place to keep shared state, so the fix is to group related tools inside a class:

Swipe sideways to see the full diagram
Function tools: a connection per call 1 request check_stock check_stock update_stock check_stock DB 4 connections opened and closed for one request;multiply by every user Class tools: one shared connection inventory = InventoryTools() self.db (opened once) self.lock (one call at a time) check_stock update_stock both methods use self.db DB 1 connection opened once, reused by every tool call
Same tools, different wiring. On the left every call dials the database again. On the right the class holds one open line that every tool method shares.
03_advanced_tool_patterns.ipynb: class-based tools
import threading


class InventoryTools:
    def __init__(self):
        # One shared connection for every tool on this object.
        # The agent can run tools in parallel threads, so guard the shared
        # connection with a lock.
        self.db = connect_to_database()
        self.lock = threading.Lock()

    @tool
    def check_stock(self, product_id: str) -> str:
        """Check product stock level.

        Args:
            product_id: The product ID to check
        """
        with self.lock:
            row = self.db.execute("SELECT name, quantity FROM inventory WHERE id = ?", (product_id,)).fetchone()
        return f"{row[0]} ({product_id}): {row[1]} units in stock" if row else f"Product {product_id} not found"

    @tool
    def update_stock(self, product_id: str, quantity: int) -> str:
        """Update product stock quantity.

        Args:
            product_id: The product ID to update
            quantity: New quantity to set
        """
        with self.lock:
            cur = self.db.execute("UPDATE inventory SET quantity = ? WHERE id = ?", (quantity, product_id))
            self.db.commit()
        return f"Updated {product_id} to {quantity} units" if cur.rowcount else f"Product {product_id} not found"


CONNECTIONS_OPENED = 0
inventory = InventoryTools()
agent = Agent(model=model, tools=[inventory.check_stock, inventory.update_stock])
agent("Check stock for PROD-123 and PROD-789, set PROD-456 to 40 units, then confirm PROD-456's new level")

print(f"\n\nConnections opened for one request: {CONNECTIONS_OPENED}")
  1. __init__ Runs once when you create InventoryTools(). That's where the single connection is opened.
  2. self.lock Strands may run several tool calls at the same time on different . The makes them take turns on the shared connection.
  3. @tool on methods Tool methods work exactly like tool functions; self is hidden from the model.
  4. "… WHERE id = ?", (product_id,) A . Never build SQL with f-strings: the values come from the model, so treat them like user input (see ).
  5. tools=[inventory.check_stock, …] Pass the methods of one object. Now the same request prints 1 connection.
Try it

How many connections does your agent open?

Function tools (connection per call)

120 connections opened per minute, about 6.0 s of every minute spent just opening them.

Class tools (one shared connection)

1 connection per running copy of your app, opened once and reused for all 120 tool calls.

Assumes about 50 ms to open each connection. Connection limits (100 for PostgreSQL, 151 for MySQL by default) count connections open at the same moment, so bursts of overlapping requests hit them long before the per-minute total looks scary.

✦Think of it like this

Calling your bank's customer care with four questions. Function tools hang up after each answer and dial again, waiting through the IVR every time. Class tools stay on the same call and ask all four. Same answers, a fraction of the waiting, and the call centre's phone lines stay free for others.

This pattern fits any shared resource: API clients that keep a connection pool, file handles for reading large datasets, connections to a cache like Redis or Memcached, or login tokens that must be refreshed now and then.

Slow warehouse checks, and async tools

Two days later your product manager writes: "Users say the agent is too slow. It takes ages to tell them whether an order can ship." To answer "Can we ship 100 units of PROD-123?", the agent checks three warehouses, each with its own API that takes about 2 seconds. Called one after another:

03_advanced_tool_patterns.ipynb: the slow way
warehouse_a = check_warehouse_inventory("PROD-123", "warehouse_a")  # 2 seconds
warehouse_b = check_warehouse_inventory("PROD-123", "warehouse_b")  # 2 seconds
warehouse_c = check_warehouse_inventory("PROD-123", "warehouse_c")  # 2 seconds
# Total: 6 seconds of waiting

(The notebook's real tool names the warehouses east, west and central.) Users stare at a spinner and abandon their carts. But look closely: the three checks don't depend on each other. Warehouse B doesn't need A's answer. They could all happen at the same time.

That's what is for. When a function is marked async, Python's can pause it at each while it waits for slow input/output (like an API response) and switch to other async work on the same thread. Several async functions make progress together, taking turns whenever one is waiting.

Swipe sideways to see the full diagram
0 s1 s2 s3 s4 s5 s6 s One after another east west central ≈ 6 s Async, at the same time east west central ≈ 2 s
Waiting in parallel. Total time drops from the sum of the calls to the slowest single call. That only works because the three checks are independent.
03_advanced_tool_patterns.ipynb: async tool
import asyncio
import time


@tool
async def check_warehouse_inventory(product_id: str, warehouse: str) -> dict:
    """Check inventory at a specific warehouse.

    Args:
        product_id: Product ID to check
        warehouse: Warehouse identifier (e.g., "east", "west", "central")
    """
    await asyncio.sleep(2)  # simulate a slow API call

    data = {
        "east": {"PROD-123": 45, "PROD-456": 12},
        "west": {"PROD-123": 30, "PROD-456": 0},
        "central": {"PROD-123": 60, "PROD-456": 25},
    }
    quantity = data.get(warehouse, {}).get(product_id, 0)
    return {"warehouse": warehouse, "product_id": product_id, "quantity": quantity}


async def main():
    agent = Agent(model=model, tools=[check_warehouse_inventory])
    start = time.time()
    response = await agent.invoke_async(
        "Can we ship 100 units of PROD-123? Check all warehouses: east, west, and central."
    )
    elapsed = time.time() - start
    print("\n\n" + response.message["content"][0]["text"])
    print(f"\nTotal time: {elapsed:.1f}s (three sequential checks alone would take ~6s)")


await main()
  1. async def Marks the tool as pausable.
  2. await asyncio.sleep(2) Stands in for a 2-second API call. While one call waits here, the others run.
  3. agent.invoke_async(…) The async way to call an agent. When the model asks for all three checks in one turn, Strands runs them .
  4. await main() Works in Jupyter, which already has an event loop. In a plain .py script use asyncio.run(main()) instead.
  5. Result 45 + 30 + 60 = 135 units, so yes, 100 can ship. The tool waiting takes about 2 seconds instead of 6; total time also includes the model's thinking.

In a real tool you'd use an async HTTP client instead of asyncio.sleep:

03_advanced_tool_patterns.ipynb: with a real async HTTP client
import httpx

@tool
async def check_warehouse_inventory(product_id: str, warehouse: str) -> int:
    """Check inventory at a specific warehouse."""
    async with httpx.AsyncClient() as client:
        response = await client.get(f"https://api.warehouse-{warehouse}.example.com/inventory/{product_id}")
    return response.json()["quantity"]
Run it

Race: sequential vs async warehouse checks

Sequential

east
west
central

0.0 s

Async

east
west
central

0.0 s

The race plays at 4× speed so you don't wait. The times shown are real-scale.

✦Think of it like this

A waiter with three tables. The slow way: take table 1's order, wait at the kitchen until it's cooked, serve it, then go to table 2. The async way: hand all three orders to the kitchen, and serve each as it's ready. The kitchen still takes the same time per dish; you just stop standing around.

iNot every tool needs async

If a tool finishes almost instantly (say under 100 ms) or only does calculations with no external calls, async adds complexity and no measurable speed. Start with regular tools; add async when slow I/O actually hurts.

Choosing your approach

Swipe sideways to see the full diagram
Start: a plain @tool function Do several tools share an expensive resource?a DB connection, an API client, a login token yes Group them in a class no / also Does it wait on slow I/O, many calls at once?network APIs, remote services yes Make it async Need strict input rules (formats, allowed values)? yes Explicit tool spec (TOOL_SPEC)
Let real problems guide you, not theoretical ones. The answers aren't exclusive: a class can hold async tool methods if you have both problems.

Start with the decorator: it's simple and covers most cases. Build, test, deploy, and watch what happens. Connection trouble? Group the tools in a class. Slow external calls? Make them async so they run concurrently.

One more option: if a tool needs strict validation of its inputs (a specific format, or only certain allowed values), Strands lets you write an explicit input schema yourself, called a TOOL_SPEC, instead of deriving it from type hints. It's an advanced pattern we won't need in this course. In fact you've already met one: use_aws is defined with a TOOL_SPEC.

2.6

Quick start with Strands Agent Builder

You can now write tools by hand, use prebuilt ones and reach AWS. But when you just want to test an idea quickly, writing the imports and setup every time slows you down. Agent Builder removes that friction.

Agent Builder is a : you describe what you want in plain English, and it builds and tests it for you. No imports, no setup. It's made for rapid prototyping and learning.

✦Think of it like this

Training wheels on a bicycle. You get to ride and explore right away without worrying about balance. Once you're confident, the training wheels come off and you write your own agents with full control.

Install it with (or pip), then start it:

terminal
pipx install strands-agents-builder     # or: pip install strands-agents-builder
strands                                 # interactive mode
strands "Build a tool that converts Celsius to Fahrenheit"
Watch

An Agent Builder session (illustrative)

terminal

A replay of what a session looks like. The exact wording and generated code will differ each time you run it.

Ask it to "Build a tool that converts Celsius to Fahrenheit", and Agent Builder will:

  1. 1

    Create the conversion tool

  2. 2

    Test it with an example

    For instance, "What's 100°C in Fahrenheit?"

  3. 3

    Show you the result

    100°C is 212°F.

  4. 4

    Let you keep trying more conversions

Two modes

⇄Interactive mode

Run strands and have a conversation with the builder. Ask it to create tools, test agents or explain concepts.

→Command mode

Give one instruction directly:
strands "Build a tool that monitors API rate limits across all services and alerts at 80% capacity"

What it comes with

Kind of agentBuilt-in tools it uses
Data processingCalculator, Python REPL, file editor
Web-connectedHTTP requests, Slack
AWS-integrateduse_aws to manage cloud resources
Multi-agentswarm and workflow to coordinate several agents
CreativeImage generation and video creation powered by Amazon Bedrock

Under the hood

Swipe sideways to see the full diagram
1 Registerits built-in toolswith Strands 2 Set up modelon Bedrock, tunedfor tool use 3 Your requestgoes to the model,which picks tools 4 Strands runsthe tools, returnsresults to the model 5 Model writesa natural-languageanswer the same agent loop you've been using all along
No magic. Agent Builder is a Strands agent with a big toolbox and a good system prompt. It just does the setup for you.
⌘Point it at the course model

Agent Builder uses Amazon Bedrock by default and reads the model ID from the STRANDS_MODEL_ID environment variable. Its built-in default is an older Claude Sonnet model, so set it to the one the course uses:

terminal
export STRANDS_MODEL_ID=global.anthropic.claude-sonnet-4-6
strands

Keeping what you build

Agent Builder runs interactively and does not save the generated tool code to files on its own. Two ways to keep your work:

  • Start it with --kb <knowledge-base-id> (or set STRANDS_KNOWLEDGE_BASE_ID) to store and retrieve tools across sessions using an Amazon Bedrock .
  • Copy the generated code from the session into your own Python files.

Agent Builder is great for learning and experiments. For production applications you'll want the full control of writing agents yourself, with the patterns from this section.

2.7

Tools in LangChain

If you've searched for how to build agents, you've met LangChain. It's been around longer and has a big community, so it's worth knowing how it compares.

Both frameworks do the same core thing: turn Python functions into tools and let agents use them. The difference is approach. Strands is model-driven: you write minimal code and let the LLM work out the workflow. LangChain gives you more explicit control over how the agent executes, which means more setup code but more ways to customise. You already ran a LangChain agent in Section 1; here is its tool next to one from this section:

Strands (this section, shortened)
from strands import Agent, tool

@tool
def check_inventory(product_id: str) -> str:
    """Check if a product is in stock.

    Args:
        product_id: The product ID to check (e.g., "PROD-123")
    """
    ...

agent = Agent(model=model, tools=[check_inventory])
agent("Is PROD-123 in stock?")
LangChain (excerpt, section-01 hello_langgraph_agent.py)
from langchain.tools import tool

# A simple tool the agent can call
@tool
def greet(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}! Welcome to the world of AI agents."

agent = create_agent(model=llm, tools=[greet], system_prompt="You are a friendly assistant.")

response = agent.invoke({"messages": [{"role": "user", "content": "Please greet Alice and Bob."}]})
Strands AgentsLangChain
Making a tool@tool + type hints + docstring@tool + type hints + docstring (same idea)
ApproachModel-driven: minimal code, the LLM plans the workflowExplicit control over execution; more setup, more customisation
AWS fitNative: Bedrock, use_aws, Lambda, S3 out of the boxVia langchain-aws integrations
EcosystemGrowing, AWS-backedLarger and older community

We use Strands in this course because it's simpler to learn and fits naturally with AWS services like Bedrock, Lambda and S3. If you work in another environment or need LangChain's larger ecosystem, the tool ideas you learned here transfer directly.

2.8

Recap and self-check

Your agents no longer just talk about solutions. They carry them out.

We started with the problem: LLMs are great at generating text but can't check databases, call APIs or touch the real world. We solved it with tools. You turned Python functions into tools with @tool, gave one agent several tools and watched it choose the order, used the prebuilt Strands toolbox, and reached S3, DynamoDB and Lambda in plain English with use_aws. You built custom tools for your own logic, learned when to group tools in a class or make them async, and saw Agent Builder do the setup for you.

You should now be able to explain

IdeaIn one line
Why toolsWithout them an agent tells you how; with them it does it
Brain, coordinator, handsThe LLM decides, the agent coordinates, tools act
Function to tool@tool + type hints + docstring; the model sees name, description and input schema, never the body
Many toolsSmall single-purpose tools; the agent picks the order and passes data between them
Tool ecosystemCommunity package, custom tools, MCP tools, AWS integrations, all in one tools=[…] list
use_awsEnglish to boto3 calls; confirmation for changes; IAM still decides what's allowed
Class toolsShare an expensive resource (like one DB connection) across tool methods
Async toolsIndependent slow calls run concurrently: total time ≈ slowest call, not the sum
Agent BuilderA ready-made Strands agent for quick experiments; write your own for production

When something goes wrong

What you seeWhat it usually means and how to fix it
"…is deprecated" warnings from calculator, http_request or current_timeExpected with strands-agents-tools 0.8.x. The tools still work; carry on.
The notebook pauses asking you to confirm a file write or AWS changeThat's tool consent. Answer in the prompt, or set os.environ["BYPASS_TOOL_CONSENT"] = "true" as notebook 2 does (sandbox only).
AccessDenied from S3, DynamoDB, Lambda or IAMYour user or role lacks that permission. Use an admin sandbox user or attach setup/iam-policy-course.json.
Lambda creation fails with "role … cannot be assumed"The new IAM role hadn't propagated yet. Wait a few seconds and re-run the setup cell (it already sleeps 10 s).
The agent says it can't do something you gave it a tool forCheck the tool is in tools=[…] and the docstring clearly says what it does and when to use it.
ModuleNotFoundErrorRun the %pip install -q -r requirements.txt cell, then restart the kernel.
!Did you clean up?

If you ran notebook 2, make sure its last cell (3.5) ran: it deletes the demo S3 bucket, DynamoDB table, Lambda function and IAM role. Notebook 3's SQLite file (inventory_demo.db) is local and costs nothing.

Self-check

Ten quick questions. Pick an answer to see why it's right or wrong.

1. You ask an agent with no tools how many open issues your GitHub repo has. What do you get?

2. Which three things turn a plain function into a Strands tool?

3. What does the model actually see of your tool?

4. In the sales assistant, who decided to call get_sales_data, then analyze_sales, then send_email?

5. Why prefer several focused tools over one mega-tool?

6. use_aws is about to call dynamodb.update_item. What happens by default?

7. You're signed in as a read-only user and ask the agent to update a DynamoDB item. What happens?

8. Your agent opens a new database connection on every tool call. The fix?

9. Three independent warehouse APIs each take 2 seconds. As async tools called in one turn, roughly how long do the checks take?

10. Should a tool that adds two numbers locally be async?

Score: 0 of 10 answered

→Up next: Section 3, agent memory

Your agents can act, but they forget everything between conversations. Next you'll fix that with short-term and long-term memory and context-management strategies, using Mem0 and Amazon Bedrock AgentCore Memory, and build a personalised agent that remembers a user's preferences across sessions. Folder: section-03-agent-memory.

A–Z

Glossary

Every underlined word on this page, in one place.

AI agent

Software that uses an LLM to plan, call tools, look at results and keep working towards a goal.

Amazon DynamoDB

A fully managed AWS NoSQL database. Items are looked up by key, like customer_id = 12345.

Amazon S3

AWS object storage: files (objects) kept in buckets, read and written over an API.

API

Application programming interface: a defined way for one program to ask another to do something, usually over the network.

async

Marks a Python function that can pause while waiting (for a network reply, say) so other work runs meanwhile.

await

Inside an async function: "wait here for this result, and let other tasks run in the meantime".

AWS Lambda

Run a function in the cloud without managing servers. You pay only while it runs.

boto3

The official AWS SDK for Python. Every AWS call in this course, including the ones use_aws makes, goes through it.

CLI

Command-line interface: a program you use by typing commands in a terminal, like aws, pip or strands.

Concurrency

Several tasks making progress in overlapping time, for example three API calls all waiting at once.

CSV

Comma-separated values: a plain-text table, one row per line, columns separated by commas.

Decorator

A line starting with @ placed above a Python function to add behaviour. @tool registers a function as an agent tool.

Deprecation

A warning that a feature still works but will be removed or replaced in a future version.

Docstring

The text in triple quotes at the top of a function. For tools, it becomes the description the model reads to decide when to call it.

Environment variable

A named setting that lives outside your code, like BEDROCK_MODEL_ID or BYPASS_TOOL_CONSENT. Programs read it at run time.

Event loop

The scheduler that runs async functions, switching to whichever one is ready whenever another is waiting.

Exception

Python's way of signalling an error. try/except catches it so your code can respond instead of crashing.

Function calling (tool use)

The model's ability to say "run this function with these arguments" instead of only writing text. The agent runs it and returns the result.

HTTP status code

A number a server returns with every response: 200 means OK, 404 not found, 500 server error.

IAM

AWS Identity and Access Management: decides who can do what in your AWS account. An agent can never do more than the credentials it runs with.

Intent classifier

A model trained to map a sentence to a fixed label like CHECK_INVENTORY. Traditional chatbots need one; tool-using agents don't.

JSON

A simple text format for structured data: {curly braces} for objects, [square brackets] for lists. Most APIs speak it.

Knowledge base (Bedrock)

A managed Amazon Bedrock store that indexes your documents for retrieval. Agent Builder can use one to keep tools between sessions.

Large language model (LLM)

A model trained on huge amounts of text that understands and writes language. The agent's brain.

Lock

A guard that lets only one thread use a shared resource at a time, so two tool calls don't trample the same database connection.

MCP (Model Context Protocol)

An open standard for exposing tools and data to AI apps. Any MCP server can be plugged into a Strands agent. Covered in Section 5.

Parameterised query

SQL with placeholders (?) whose values are passed separately. The database treats them as data, never as SQL code.

pipx

Installs a Python command-line app into its own isolated environment, so its libraries don't clash with your projects.

Regex (regular expression)

A pattern language for matching text, like PROD-\d+ to find product IDs. Precise but brittle: it only matches what you anticipated.

REST API

A common style of web API: send an HTTP request (GET, POST…) to a URL and get JSON back.

SQL injection

An attack where text meant as data is run as SQL. Building queries with f-strings invites it; parameterised queries prevent it.

SymPy

A Python library for symbolic maths: exact arithmetic, solving equations, derivatives and integrals. The calculator tool is built on it.

Thread

A separate line of execution inside one program. Strands can run several tool calls on different threads at once.

Timeout

The longest you're willing to wait for a response. requests.get(…, timeout=5) gives up after 5 seconds instead of hanging forever.

Tool

A function or service an agent can call to do something it can't do with words alone: fetch data, compute exactly, change a system.

Type hints

Annotations like server_url: str or -> dict that say what type a value should be. Strands turns them into the tool's input schema.

© 2026 Shivank Agrawal, Future with Shivank. Agentic AI on AWS, Section 2. Course code is provided under the MIT License.

AWS service names, tool names and library versions change often. When something here differs from what you see, the current documentation wins.