
How to Build an AI Agent on AWS Using AWS AgentCore
Build a tool-using agent with Strands and Amazon Nova Pro, test it locally, then deploy it as a serverless endpoint on AgentCore Runtime and call it from Lambda.
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.

Callers reach the agent through a single HTTP endpoint on AgentCore Runtime, where the Strands agent loops between its tools and Amazon Nova Pro on Bedrock. The bottom row shows the one-time deployment path through CodeBuild and ECR.
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:
1
2
3
mkdir first-agent && cd first-agent
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activateCreate
requirements.txt:1
2
3
strands-agents
strands-agents-tools
bedrock-agentcoreInstall the dependencies, plus the starter toolkit, which provides the
agentcore CLI used for deployment:1
2
pip install -r requirements.txt
pip install bedrock-agentcore-starter-toolkitThe 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:
1
2
3
4
first-agent/
├── agent.py # agent, tools, model and AgentCore entrypoint
├── requirements.txt # runtime dependencies
└── invoke_lambda.py # optional: calling the deployed agent from LambdaStep 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.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# agent.py (Step 1)
from strands import Agent, tool
from strands_tools import calculator, current_time
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:
1
python agent.pyIf 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):
1
2
3
4
5
6
7
8
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.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 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.
1
2
3
4
5
6
7
8
9
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:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
"""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
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()
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 8080Test locally with two terminals. In the first, start the service:
1
python agent.pyIn the second, check health, then send a prompt:
1
2
3
4
5
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:
1
2
3
{"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.1
agentcore configure --entrypoint agent.py --name first_agentThis 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.1
agentcore deployWhen it finishes, the toolkit prints the agent runtime ARN. Check the status at any time:
1
agentcore statusExpected output (abridged; the account ID and ARN suffix are placeholders):
1
2
Deployment completed successfully
Agent ARN: arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/first_agent-AbCdEf1234Invoke from the CLI:
1
agentcore invoke '{"prompt": "What is 15% of 2,480?"}'1
2
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.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 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'?"}:1
2
3
4
5
{
"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:
1
agentcore destroyThis 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.
In my next article, I will add Memory and Gateway to this same agent. If you build along, share what you make.
References
- Strands Agents documentation
- Deploying Strands agents to Amazon Bedrock AgentCore Runtime
- Amazon Bedrock AgentCore Developer Guide
- Bedrock AgentCore starter toolkit on GitHub
- Workshop: Build Your First AI Agents with AWS AgentCore, EduCloud Academy
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article