AWS Builder Center
Once Upon Agentic AI (Part 1): Create your own agent and forge its tools with Strands Agents

Once Upon Agentic AI (Part 1): Create your own agent and forge its tools with Strands Agents

Once upon a time, there was a world built not of stone and steel, but of code. In this realm, every function was an incantation, every bug a curse, every deployment a battle.

Series: Once Upon Agentic AI (1 article)

  1. 1
    Once Upon Agentic AI (Part 1): Create your own agent and forge its tools with Strands Agents This article
Developer Advocate @ AWS
On Day1, three heroes that nothing destined to cross went on a journey to write the same legend.  TheWitcherish  from the Order of Developers,  Amagash  from the Order of Agents, and  Tagazok  from the Order of Builders. And high above it all, an  Orchestrator  agent watched over this world.
Three heroes - TheWitcherish, Amagash, Tagazok

The series follows the adventure in three parts:
  • This article: Create your first agent, equip it with a Vended tool, then write a tool of your own.
  • Part 2: MCP (Model Context Protocol) explored via the Strands Agents Harness SDK.
  • Part 3: RAG (Retrieval-Augmented Generation), A2A (Agent2Agent) and structured output with an orchestrator agent leading specialized agents.
By the end of Part 1, your agent tells a story, looks up a spell in a real rules API, and rolls real dice with a power you wrote. You'll also be able to prove each of those from the log.

Get started

To complete our heroes quest, you need Python,  uv , a terminal, and AWS credentials with access to Amazon Bedrock. As our three heroes share one adventure, you write the code behind each step of their quest with  Strands Agents , an open source SDK for AI agents in Python and TypeScript.
The code lives in  aws-samples/sample-once-upon-agentic-ai . The  main  branch contains skeletons with  # TODO  markers.
git clone https://github.com/aws-samples/sample-once-upon-agentic-ai.git ~/sample-once-upon-agentic-ai
cd ~/sample-once-upon-agentic-ai
uv sync
export AWS_PROFILE=YOUR_AWS_PROFILE AWS_REGION=YOUR_AWS_REGION
No AWS profile yet? The Strands Agents  Amazon Bedrock model provider guide  shows how to set up AWS credentials (with  aws configure , environment variables, a boto3 session, or  aws login ) and lists the IAM permissions your agent needs to invoke a model. Strands picks those credentials up on its own.

Why do we call AI Agents... agents?

Well, agents do have agency, that is why we call them agents. Both words come from the Latin agere, "to act". A model on its own only answers. An agent decides what to do next and acts on it, using the tools you give it.
An agent has three parts: a model, a system prompt with its instructions, and tools it may use. When you send a message, the model decides whether to call a tool. It reads the result, then answers or calls another tool. That cycle is the agent loop. Strands runs it for you, handling model calls, tool runs, and conversation history while you write the instructions and tools.
The agent loop

Here is what each part gives our heroes:
Our heroes need to...
Which means...
Strands gives us...
Think and decide
A brain that reads and chooses what to do
A model on Amazon Bedrock (default model provider)
Know who they are
A standing instruction
A system prompt
Act beyond words
Reaching outside the model
Tools: ready-made ones from  strands.vended_tools , or your own with  @tool 
Step 1 gives the model its system prompt. Steps 2 and 3 give it tools.

Step 1: Does an agent need more than one line?

"Our heroes had just arrived in the realm. No map, no weapons, not a single spell." Your first agent arrives the same way: a voice, and nothing in its hands.
Summons it with debug logs, a system prompt, and one call:
from strands import Agent
import logging

logging.getLogger("strands").setLevel(logging.DEBUG)
logging.basicConfig(
format="%(levelname)s | %(name)s | %(message)s",
handlers=[logging.StreamHandler()]
)

agent = Agent(
system_prompt="You are a game master for a fantasy role-playing game"
)

response = agent("Hi, I am an adventurer ready for adventure!")
 Agent(system_prompt=...)  gives the model an instruction for every turn. Here it makes the agent the storyteller of a fantasy role-playing game. Without a model argument, Strands uses Amazon Bedrock with Claude Sonnet 4.6 by default. Bedrock is a serverless AWS service that runs models from several providers for you behind its APIs, so trying another model means changing a model ID, not rewriting your agent.
cd ~/sample-once-upon-agentic-ai/1_strands_basics
uv run python simple_agent.py
The debug logs show what the SDK does:
DEBUG | strands.models.bedrock | config=<{'model_id': 'global.anthropic.claude-sonnet-4-6', ...}> | initializing
DEBUG | strands.tools.registry | tool_count=<0> | tools configured
DEBUG | strands.models.bedrock | request=<{'modelId': 'global.anthropic.claude-sonnet-4-6', 'messages': [...], 'system': [...]}>
DEBUG | strands.event_loop._retry | stop_reason=<end_turn> | model call succeeded, ...
One request, no tools, and  stop_reason=<end_turn> : the model finished in one turn. The agent opened a scene:
The torchlight flickers as you step into the worn wooden tavern. The smell of ale and woodsmoke fills the air. A grizzled barkeep eyes you from across the room.
Then the barkeep added: "We don't get many fresh faces in Thornhaven these days." One sentence summoned a tavern and named a town. No map, no weapons:  tool_count=<0> . Keep  DEBUG  on. The agent sounds like it knows the rules, but it has no book to check them in.

Step 2: Does the agent already know Fireball?

"Before conquering anything, they first had to equip themselves." Our heroes enter a shop that promises everything they need. One of them is hungry. "No, we don't do burgers here," the shopkeeper answers, "but there's plenty of other stuff that should interest you." Our heroes expected a few trinkets; they found a treasure.
A tool is a function described to the model, which decides whether to call it. A vended tool comes ready-made in  strands.vended_tools . Here,  http_request  calls web APIs:
from strands.vended_tools import http_request

agent = Agent(tools=[http_request], system_prompt=...)
The system prompt points to  Open5e , an open fantasy-RPG rules API, then asks: "What does the Fireball spell do, and how much damage does it deal?" Copy step 1's logging block, then run:
cd ~/sample-once-upon-agentic-ai/2_vended_tools
uv run python agent_with_vended_tools.py
Here is the trace of what the SDK does:
DEBUG | strands.tools.registry | tool_count=<1> | tools configured
DEBUG | strands.event_loop._retry | stop_reason=<tool_use> | model call succeeded, ...
DEBUG | strands.tools.executors._executor | tool_use=<{..., 'name': 'http_request', 'input': {'method': 'GET', 'url': 'https://api.open5e.com/v2/spells/srd_fireball/'}}> | streaming
Tool #1: http_request
The first turn ends with  stop_reason=<tool_use> . The model built the URL, Strands ran it, and the result began  {"status": 200, "status_text": "OK", ...} . Only then did the agent answer:
Casting Time: Action Range: 150 feet ... Failed save: The target takes 8d6 fire damage.
The API's JSON says  "range": 150  and  "casting_time": "action" ; the reply repeats both. You wrote no code to choose the tool: the model chose it, and the log proves it. But a shop only sells tools someone else made.

Step 3: Is a function already a tool?

"Saddlebags full and hearts light, our heroes continued on their way." Two of them stride ahead. Behind them, TheWitcherish leafs through the grimoire he has just acquired.
Suddenly, he shouts "WAIT!" and announces an extraordinary power. The power: "It's 13h37." The party is not impressed. Then comes a better idea: "you can write your own powers!" How? "All we have to do is create a function, then declare it as a tool."
You turn a function into a tool with a decorator, a line beginning with  @  above the function. In Strands, it is  @tool . The  custom tools docs  explain how Strands reads the function's docstring and type hints to build the tool specification shown to the model.
Our custom tool is a dice roller, trimmed here:
from strands import Agent, tool

@tool
def roll_dice(faces: int = 6) -> int:
"""Roll one die with a given number of faces and return the result.
...
"""
import random
return random.randint(1, faces)

dice_master = Agent(tools=[roll_dice], system_prompt=...)
The system prompt makes the agent "Lady Luck", who rolls four dice for each ability score and drops the lowest. When the agent is built, the log shows the new tool being registered:
DEBUG | strands.tools.registry | tool_name=<roll_dice>, tool_type=<function>, is_dynamic=<False> | registering tool
DEBUG | strands.tools.registry | tool_count=<1> | tools configured
agent built, tools: ['roll_dice']
The solution prints  roll_dice.tool_spec , the specification sent to the model:
{'name': 'roll_dice', 'description': 'Roll one die with a given number of faces and return the result.\n\n...', 'inputSchema': {'json': {'properties': {'faces': {'default': 6, ..., 'type': 'integer'}}, 'type': 'object'}}}
The docstring became the description, the type hint became  'type': 'integer' , and the default became  'default': 6 . The model reads this specification, not your code, so a clear docstring helps it choose the tool.

Time to roll the dice!

Run the chapter:
cd ~/sample-once-upon-agentic-ai/3_custom_tools
uv run python agent_with_dice_roll_tool.py
The trace shows each step the SDK takes:
DEBUG | strands.event_loop._retry | stop_reason=<tool_use> | model call succeeded, ...
DEBUG | strands.tools.executors._executor | tool_use=<{..., 'name': 'roll_dice', 'input': {'faces': 6}}> | streaming
...
Tool #1: roll_dice
...
Tool #16: roll_dice
DEBUG | strands.event_loop._retry | stop_reason=<end_turn> | model call succeeded, ...
Lady Luck requested all 16 dice in one turn. Strands ran the tool 16 times, returned the results, and the second model call answered:
STRENGTH Rolls: 3, 1, 1, 3 -> Drop the 1 (one of them) -> 3 + 1 + 3 = 7 ... CHARISMA Rolls: 2, 6, 5, 3 -> Drop the 2 -> 6 + 5 + 3 = 14
The 16 tool results in the full log carry every one of those numbers, in the same groups Lady Luck used: 3, 1, 1, 3; 1, 2, 4, 3; 3, 2, 1, 2; and 2, 6, 5, 3.
The log is the record. When I asked step 1's tool-free agent the Fireball question, it also said 8d6, so the reply alone can't tell you whether a tool ran.  tool_count ,  stop_reason  and each  Tool #  line can.
Tagazok decided to write his own custom tool decorated with  @tool :  def fire_sword():  That is AWSome!
Once Upon Agentic AI - Strands Agents Comic Book


Follow the adventurers

In Part 2, our heroes seek help beyond their own strength. We equip them with an MCP server as another monster, greater and mightier, awaits them!
Until then, three habits for your own agent:
  • Start with the system prompt. Settle the agent's role and rules before you add tools.
  • Write the docstring for the model. Read the  tool_spec  before the model does.
  • Let the model choose. If you catch yourself writing  if  statements to pick a tool, that rule belongs in the system prompt.
P.S.: Their adventure is also a comic book. If you would like your own copy, the  Once Upon Agentic AI with Strands Agents  page lists where to buy it in North America, Europe and Asia Pacific.
The comic book illustrations in this blog post were created and produced by  Holy Bird , based on the AWS workshop. All rights reserved.

Series: Once Upon Agentic AI (1 article)

  1. 1
    Once Upon Agentic AI (Part 1): Create your own agent and forge its tools with Strands Agents This article
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article