Section 2: Giving agents tools
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

    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

    Roughly what the model is shown

    What that means

    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

    Tool result (computed for real)

    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

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

      Agent with check_inventory

      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)

      Class tools (one shared connection)

      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

      Async

      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.

      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.

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