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.
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, where an agent answers from documents it looks up) and A2A (Agent2Agent, a protocol that lets agents talk to other agents), 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.
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!
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_specbefore the model does. - Let the model choose. If you catch yourself writing
ifstatements 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.



