Introduction
In under 100 lines of Python, you can build an AI agent that picks its own tools, reasons with Amazon Nova Pro on Amazon Bedrock, and runs as a serverless HTTP endpoint on Amazon Bedrock AgentCore Runtime. This article walks through that build step by step, from an empty folder to a deployed agent you can call from AWS Lambda.
I built this while following the Build Your First AI Agents with AWS AgentCore workshop from EduCloud Academy. I have expanded the session into a complete, reproducible guide, checked the code against the current SDK releases, and added the pitfalls I hit along the way.
Why agents need a serverless home. An agent's resource use is unpredictable: one request may answer instantly, the next may chain five tool calls and several model invocations. Paying for idle servers makes little sense for that pattern. AgentCore Runtime runs each session in an isolated microVM, scales to zero, and bills only for active consumption.
What AgentCore adds beyond hosting. AgentCore is a set of services for running agents in production: Runtime (hosting), Gateway (turning APIs and MCP servers into agent tools), Identity (safe access to external services on a user's behalf), Memory, Observability and more. This guide uses Runtime; the others plug into the same agent later.
What you will build
- A Strands agent with two community tools (
calculator,current_time) and one custom tool (letter_counter). - Debug logging that shows every model call and tool choice.
- An explicit Amazon Nova Pro model on Bedrock.
- An AgentCore-compatible HTTP service, tested locally with
curl. - A cloud deployment on AgentCore Runtime, invoked from the CLI and from Lambda.
Tested with: Python 3.10+, strands-agents 1.57, strands-agents-tools 0.8, bedrock-agentcore 1.24 and bedrock-agentcore-starter-toolkit 0.3.13 (October 2026). These SDKs move fast, so check the versions if a command differs.
Architecture
Every request takes the same path: a caller sends JSON to the AgentCore endpoint, the Strands agent loops between Nova Pro and its tools, and a plain JSON answer comes back.
The upper half is the request path; the lower half is the one-time deployment path that packages agent.py into a container AgentCore can run.
| Component | Role in this build |
|---|---|
| Strands Agents SDK | Open-source Python framework that runs the agent loop and tool calls |
| Amazon Bedrock (Nova Pro) | Managed foundation model that does the reasoning, via the Converse API |
bedrock-agentcore SDK |
Wraps the agent in an HTTP service with /invocations and /ping
|
| AgentCore Runtime | Serverless host: one isolated microVM per session, billed on active use |
| AgentCore starter toolkit | CLI that configures, builds (CodeBuild), stores (ECR) and deploys the agent |
| AWS Lambda / Amazon ECS | Example callers that invoke the deployed agent through the AWS SDK |
| Amazon CloudWatch | Receives the runtime's logs and traces |
Prerequisites and project setup
You need an AWS account, credentials configured locally, Python 3.10 or newer, and access to Amazon Nova Pro in your Region.
| Requirement | How to check or set it up |
|---|---|
| AWS CLI v2 with credentials |
aws sts get-caller-identity returns your account ID |
| Python 3.10+ | python3 --version |
| Bedrock model access | Bedrock console → Model access → enable Amazon Nova Pro (us-east-1 used here) |
| IAM permissions (local dev) |
bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream on the Nova Pro inference profile |
| IAM permissions (deploy) | Rights to create IAM roles, ECR repositories, CodeBuild projects and AgentCore runtimes (the toolkit creates these for you) |
Create the project and a virtual environment:
mkdir first-agent && cd first-agent
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
Create requirements.txt:
strands-agents
strands-agents-tools
bedrock-agentcore
Install the dependencies, plus the starter toolkit, which provides the agentcore CLI used for deployment:
pip install -r requirements.txt
pip install bedrock-agentcore-starter-toolkit
The toolkit is a development tool, so it stays out of requirements.txt; the runtime container only needs the three packages above. Install everything inside the active virtual environment. In the workshop, the most common stumble was a package installed globally and missing at runtime.
The final project is deliberately small:
first-agent/
├── agent.py # agent, tools, model and AgentCore entrypoint
├── requirements.txt # runtime dependencies
└── invoke_lambda.py # optional: calling the deployed agent from Lambda
Step 1: A Strands agent with tools
A Strands agent is a model, a list of tools and a prompt; the framework runs the loop that lets the model decide which tool to call. Start with two community tools and one custom tool.
The custom tool counts letters in a word. It is a classic example because language models are unreliable at counting characters, while a five-line function is exact. The @tool decorator turns the function's signature and docstring into the tool specification the model reads, so a clear docstring matters.
# agent.py (Step 1)
from strands import Agent, tool
from strands_tools import calculator, current_time
@tool
def letter_counter(word: str, letter: str) -> int:
"""Count how many times a single letter appears in a word (case-insensitive).
Args:
word: The word or text to search.
letter: The single letter to count.
"""
if not isinstance(letter, str) or len(letter) != 1:
raise ValueError("letter must be exactly one character")
return word.lower().count(letter.lower())
agent = Agent(tools=[calculator, current_time, letter_counter])
if __name__ == "__main__":
agent("What is the current time in UTC?")
agent("What is 3111696 divided by 74088?")
agent("How many times does the letter 'r' appear in 'strawberry'?")
Run it:
python agent.py
If you see ModuleNotFoundError: No module named 'strands_tools', the tools package went to another environment. Activate .venv and rerun pip install -r requirements.txt.
Expected output (the agent streams its answer to the terminal; wording varies between runs):
Tool #1: current_time
The current time in UTC is 2026-10-04T14:32:07+00:00.
Tool #2: calculator
3111696 divided by 74088 equals 42.
Tool #3: letter_counter
The letter 'r' appears 3 times in 'strawberry'.
Each prompt was routed to a different tool. That routing is the core agent loop: the model reads the tool specifications, chooses one, Strands executes it, and the result goes back to the model to write the final answer.
A note on the model. No model is configured in this snippet, but the agent is still calling one. When you omit model, Strands defaults to a Claude model on Amazon Bedrock (global.anthropic.claude-sonnet-4-6 in version 1.57). That default needs Bedrock access for that model, which is a good reason to set the model explicitly, as Step 2 does.
Step 2: Debug logging and an explicit Bedrock model
Two changes make the agent production-ready in spirit: logs that show its decisions, and a model you chose on purpose.
Logging. Setting the strands logger to DEBUG prints model configuration, tool registration and each step of the event loop. That visibility pays off most in multi-agent systems, where the trace shows exactly which agent or tool misbehaved.
Model. BedrockModel wraps the Bedrock Converse API. Here it targets Amazon Nova Pro through the US cross-region inference profile us.amazon.nova-pro-v1:0, which routes requests across US Regions for better availability. A low temperature keeps tool-heavy answers consistent.
# agent.py (Step 2): additions shown at the top of the file
import logging
import os
from strands import Agent, tool
from strands.models import BedrockModel
from strands_tools import calculator, current_time
# Strands debug logs show every model call and tool selection
logging.getLogger("strands").setLevel(logging.DEBUG)
logging.basicConfig(
format="%(levelname)s | %(name)s | %(message)s",
handlers=[logging.StreamHandler()],
)
AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
MODEL_ID = os.getenv("MODEL_ID", "us.amazon.nova-pro-v1:0") # cross-region inference profile
# ... letter_counter tool from Step 1 ...
model = BedrockModel(model_id=MODEL_ID, region_name=AWS_REGION, temperature=0.3)
agent = Agent(
model=model,
tools=[calculator, current_time, letter_counter],
system_prompt="You are a concise assistant. Use a tool whenever one fits the request.",
)
Reading the Region and model ID from environment variables means you can switch to another model, such as Claude or Nova Lite, without touching code.
Expected output. The first lines below are what Strands 1.57 prints at start-up; the event-loop lines are abridged and vary by version.
DEBUG | strands.models.bedrock | config=<{'model_id': 'us.amazon.nova-pro-v1:0', 'include_tool_result_status': 'auto', 'temperature': 0.3}> | initializing
DEBUG | strands.models.bedrock | region=<us-east-1> | bedrock client created
DEBUG | strands.tools.registry | tool_name=<calculator>, tool_type=<function>, is_dynamic=<False> | registering tool
DEBUG | strands.tools.registry | tool_name=<current_time>, tool_type=<function>, is_dynamic=<False> | registering tool
DEBUG | strands.tools.registry | tool_name=<letter_counter>, tool_type=<function>, is_dynamic=<False> | registering tool
DEBUG | strands.event_loop.event_loop | ... | streaming messages
Tool #1: letter_counter
DEBUG | strands.event_loop.event_loop | ... | tool execution completed
The letter 'r' appears 3 times in 'strawberry'.
With the trace on, you can see the full path of a request: model configured, tools registered, model asked, tool chosen and executed, answer produced. When something breaks later, this is the first place to look.
Step 3: Wrap the agent for AgentCore Runtime and test locally
AgentCore Runtime expects an HTTP service with two routes: POST /invocations for requests and GET /ping for health checks, on port 8080. The bedrock-agentcore SDK provides both through four lines of code: import the app, initialise it, decorate an entrypoint, and run it.
Here is the complete agent.py:
"""First AI agent: Strands Agents + Amazon Bedrock (Nova Pro) on AgentCore Runtime."""
import logging
import os
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel
from strands_tools import calculator, current_time
# Strands debug logs show every model call and tool selection
logging.getLogger("strands").setLevel(logging.DEBUG)
logging.basicConfig(format="%(levelname)s | %(name)s | %(message)s", handlers=[logging.StreamHandler()])
logger = logging.getLogger("first-agent")
AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
MODEL_ID = os.getenv("MODEL_ID", "us.amazon.nova-pro-v1:0") # cross-region inference profile
@tool
def letter_counter(word: str, letter: str) -> int:
"""Count how many times a single letter appears in a word (case-insensitive).
Args:
word: The word or text to search.
letter: The single letter to count.
"""
if not isinstance(letter, str) or len(letter) != 1:
raise ValueError("letter must be exactly one character")
return word.lower().count(letter.lower())
model = BedrockModel(model_id=MODEL_ID, region_name=AWS_REGION, temperature=0.3)
agent = Agent(
model=model,
tools=[calculator, current_time, letter_counter],
system_prompt="You are a concise assistant. Use a tool whenever one fits the request.",
)
app = BedrockAgentCoreApp()
@app.entrypoint
def invoke(payload: dict) -> dict:
"""AgentCore entrypoint: receives the JSON body of each /invocations request."""
prompt = payload.get("prompt", "Hello! What can you do?")
logger.info("Received prompt: %s", prompt)
result = agent(prompt)
# AgentResult is not a clean API response; return plain text instead
return {"result": str(result)}
if __name__ == "__main__":
app.run() # serves POST /invocations and GET /ping on port 8080
Test locally with two terminals. In the first, start the service:
python agent.py
In the second, check health, then send a prompt:
curl http://localhost:8080/ping
curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"prompt": "How many times does the letter s appear in Mississippi?"}'
Expected output:
{"status":"Healthy","time_of_last_update":1791145026}
{"result": "The letter 's' appears 4 times in 'Mississippi'."}
The pitfall: returning the raw result. The workshop demo hit a runtime error at exactly this point, because the handler returned the agent's response object as-is. agent(prompt) returns an AgentResult, which holds the message plus metrics, traces and event-loop state. Depending on the SDK version and what that state contains, returning it directly either fails to serialise or leaks a large internal object to your callers. When I tested with bedrock-agentcore 1.24, the raw return succeeded but the response carried every metric and trace field. Returning {"result": str(result)} gives callers a small, stable contract; str() on an AgentResult yields the final text.
Step 4: Deploy to AgentCore Runtime and invoke it
The starter toolkit turns agent.py into a running cloud endpoint with two commands: configure and deploy. By default it builds an ARM64 container in AWS CodeBuild, so you do not need Docker on your machine.
Configure. Point the toolkit at your entrypoint. It detects requirements.txt and offers to create the IAM execution role and ECR repository for you; accepting the defaults is fine for a first deployment.
agentcore configure --entrypoint agent.py --name first_agent
This writes a .bedrock_agentcore.yaml file holding the agent's settings. Keep it under version control, but never commit credentials.
Deploy. In toolkit 0.3, this command replaced the older agentcore launch, which many tutorials still show.
agentcore deploy
When it finishes, the toolkit prints the agent runtime ARN. Check the status at any time:
agentcore status
Expected output (abridged; the account ID and ARN suffix are placeholders):
Deployment completed successfully
Agent ARN: arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/first_agent-AbCdEf1234
Invoke from the CLI:
agentcore invoke '{"prompt": "What is 15% of 2,480?"}'
Response:
{"result": "15% of 2,480 is 372."}
Invoke from AWS Lambda. Any AWS service that can call an API can use the agent, including Lambda and Amazon ECS. The bedrock-agentcore client's invoke_agent_runtime operation needs the runtime ARN, a JSON payload and a session ID of at least 33 characters. Requests that share a session ID reach the same isolated session, which keeps conversation context.
# invoke_lambda.py: Lambda handler that calls the deployed agent
import json
import logging
import os
import uuid
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
client = boto3.client("bedrock-agentcore", region_name=os.environ.get("AWS_REGION", "us-east-1"))
AGENT_ARN = os.environ["AGENT_RUNTIME_ARN"] # set in the Lambda configuration
def lambda_handler(event, context):
prompt = event.get("prompt", "Hello!")
session_id = event.get("session_id") or f"session-{uuid.uuid4()}" # must be 33+ characters
logger.info("Invoking agent, session=%s", session_id)
response = client.invoke_agent_runtime(
agentRuntimeArn=AGENT_ARN,
runtimeSessionId=session_id,
payload=json.dumps({"prompt": prompt}).encode(),
contentType="application/json",
accept="application/json",
)
body = json.loads(response["response"].read()) # response is a streaming body
return {"statusCode": 200, "session_id": session_id, "answer": body.get("result")}
The Lambda execution role needs bedrock-agentcore:InvokeAgentRuntime on the runtime ARN. If your Lambda runtime's bundled boto3 predates AgentCore, package a current boto3 with the function.
Expected output for the test event {"prompt": "How many vowels are in 'Cameroon'?"}:
{
"statusCode": 200,
"session_id": "session-3f6c1a9e-2b7d-4c1e-9a55-0d8e7f4b2c11",
"answer": "There are 4 vowels in 'Cameroon': a, e, o, o."
}
Troubleshooting, cost and cleanup
Most first-run failures come from environments, permissions or model access, not from the agent code.
| Symptom | Likely cause | Fix |
|---|---|---|
ModuleNotFoundError: strands_tools |
Package installed outside the virtual environment | Activate .venv, reinstall from requirements.txt
|
AccessDeniedException calling the model |
Model access not enabled, or IAM policy missing | Enable Nova Pro in Bedrock Model access; grant bedrock:InvokeModel*
|
ValidationException on the model ID |
Inference profile not available in your Region | Use a profile valid for your Region, or set MODEL_ID
|
| Raw metrics or a serialisation error in the response | Handler returns the AgentResult object |
Return {"result": str(result)}
|
agentcore launch not found |
Toolkit 0.3 renamed the command | Use agentcore deploy
|
| Invocation works locally but fails in the cloud | Execution role lacks Bedrock permissions | Add bedrock:InvokeModel* to the runtime's execution role |
ValidationException on runtimeSessionId
|
Session ID shorter than 33 characters | Prefix a UUID, as in the Lambda example |
What it costs. You pay for three things: AgentCore Runtime (billed on active CPU and memory consumption per session, nothing while idle), Bedrock model tokens for Nova Pro, and small amounts of ECR storage, CodeBuild minutes and CloudWatch logs. For a tutorial-sized test, model tokens are usually the largest line. Check the Amazon Bedrock AgentCore pricing and Amazon Bedrock pricing pages for your Region, and set an AWS Budgets alert before you experiment.
Clean up when you are done, so nothing keeps billing:
agentcore destroy
This removes the runtime and the resources the toolkit created, such as the ECR repository and CodeBuild project. Confirm in the console that no AgentCore runtime remains, and delete the CloudWatch log groups if you no longer need them.
Conclusion and next steps
You now have an agent that chooses between three tools, reasons with Amazon Nova Pro, and runs on AgentCore Runtime behind an HTTP endpoint that Lambda can call. The code is under 100 lines; the platform handles isolation, scaling and session management.
Technical takeaways
- Tools beat prompting for exact work. Counting, arithmetic and time lookups belong in functions; the model's job is to choose and explain.
- Turn on debug logging from day one. The trace shows each decision, and it becomes essential once you add more agents.
- Design the response contract. Return a small, explicit JSON shape from your entrypoint, never the framework's internal objects.
-
Test locally before deploying. Two terminals and
curlcatch most bugs in seconds instead of after a cloud build.
Lessons from the workshop beyond the code. The second half of the session was about how to grow as a cloud and AI engineer, and three points stayed with me. Real skill matters more than collecting certificates. Structured learning paths still matter, because AI tools and endless content make it easy to drift without depth. And public building, through weekend projects and blog posts like this one, creates opportunities that waiting never will.
Where to go next
- Add AgentCore Memory so the agent remembers users across sessions.
- Expose an existing API or MCP server as tools through AgentCore Gateway.
- Use AgentCore Identity to let the agent act on a user's behalf in services like GitHub or Slack.
- Add the Code Interpreter and Browser built-in tools for more autonomous tasks.
- Wire up AgentCore Observability dashboards in CloudWatch.

Top comments (0)