Build your first AI agent on Amazon Bedrock, step by step A developer walkthrough shows how to build a minimal AI agent in Python on Amazon Bedrock by hand, wiring two ordinary functions — one listing S3 buckets, one counting objects in a bucket — to a foundation model through boto3 and a manual tool-calling loop. The guide deliberately skips Bedrock knowledge bases and the managed Agents for Amazon Bedrock service so readers see the underlying request-and-respond loop, noting the script leaves no vector store, index, endpoint or server running after it exits. Your first AI agent on AWS: two tools and a loop you wrote yourself. By the end of this walkthrough you'll have a Python script that answers a question about your own AWS account by deciding, on its own, which of two functions to call. That's an AI agent: a model that can ask your code to do something, and a loop that keeps handing it the results until it has an answer. Two things we're deliberately not using. A Bedrock knowledge base https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html , which is the piece that lets a model search your own documents. And Agents for Amazon Bedrock https://docs.aws.amazon.com/bedrock/latest/userguide/agents.html , the managed service that builds an agent for you. Both are useful later. Neither shows you the loop, and the loop is the part worth understanding. Amazon Bedrock https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html is the AWS service that gives you one way to call several foundation models, which are the large pretrained models you send text to and get text back from. boto3 , the AWS library for Python. Amazon Bedrock is pay per use, so this walkthrough spends money every time it calls a model. What it costs depends on the model and on how much text goes in and comes back. Prices change, so we won't put a number in this article: read the Amazon Bedrock pricing page https://aws.amazon.com/bedrock/pricing/ before you run anything. The good news is what this doesn't create. No vector store, no index, no endpoint, no server. Nothing is left running when the script exits, and there's nothing to remember to delete. That's the main reason we're writing the loop by hand instead of standing up a knowledge base. And if you'd rather spend nothing: the two tool functions in step 3 are ordinary Python and run fine with no model anywhere near them. Read the loop, run the tools by hand, and you'll still see what an agent does. That's it. The model never runs anything itself. It asks, and your code decides whether to oblige. Open Amazon Bedrock in the AWS console and pick your Region first, top right. A Region is the part of the world where AWS runs the service, and which models you can use depends on it https://docs.aws.amazon.com/bedrock/latest/userguide/models-regions.html . Now find model access and request access to one model https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html . Access is per model and per Region, and you ask for it before your first call. Then open the model list and copy the model ID of the model you got access to. A model ID is the string you name in your code to say which model you want. We're not printing one here, because your console is the truth about what you have. What you should see: one model with access granted, and a model ID on your clipboard. pip install boto3 boto3 is the AWS SDK for Python: the library that turns a Python function call into an AWS request. It needs credentials, and if you've never set those up, the boto3 credentials guide https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html lists every place it looks. No separate check needed, because the first tool you write does that job for free. A tool is just a function. Nothing about it is special, and the model can't see inside it. Make a file called agent.py : python import boto3 REGION = "paste-your-region-here" MODEL ID = "paste-your-model-id-here" s3 = boto3.client "s3", region name=REGION bedrock = boto3.client "bedrock-runtime", region name=REGION def list buckets : """Names of the S3 buckets in this account.""" response = s3.list buckets return {"buckets": bucket "Name" for bucket in response "Buckets" } def count objects bucket name : """How many objects one listing call finds in a bucket.""" response = s3.list objects v2 Bucket=bucket name return {"bucket": bucket name, "objects": len response.get "Contents", } A bucket is the container your files live in on Amazon S3, and each file you put in one is called an object. Try the first tool now, before any model is involved. Add print list buckets at the bottom and run python agent.py . Names coming back means your credentials work, and it cost nothing, because listing S3 is not a Bedrock call. Take the print line out again. Two honest notes on count objects . It counts what one listing call returns, and a bucket with a lot of objects comes back in pages, which the boto3 reference for list objects v2 https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/list objects v2.html shows how to walk through. One call is enough today. And if you have no buckets, make one empty bucket in the S3 console so the second tool has something to count, then delete it after. The model can't read your Python. It only knows what you tell it, so every tool gets a name, a description, and a schema: a small description of the arguments it takes, written in JSON, a plain text format for structured data. Here's the catch. That description is the only thing the model has to go on when it decides whether this is the right tool, so a vague one gets ignored. TOOL CONFIG = { "tools": { "toolSpec": { "name": "list buckets", "description": "List the names of the S3 buckets in this AWS account.", "inputSchema": { "json": {"type": "object", "properties": {}, "required": } }, } }, { "toolSpec": { "name": "count objects", "description": "Count the objects in one S3 bucket. Needs the bucket name.", "inputSchema": { "json": { "type": "object", "properties": { "bucket name": { "type": "string", "description": "The name of the bucket to count.", } }, "required": "bucket name" , } }, } }, } TOOLS = {"list buckets": list buckets, "count objects": count objects} def run tool name, arguments : if name not in TOOLS: return {"error": f"There is no tool called {name}."} try: return TOOLS name arguments except Exception as problem: return {"error": str problem } run tool hands errors back to the model instead of crashing, and that's deliberate. An agent told "that bucket doesn't exist" can try another. An agent whose script has died can't. This is the whole agent, and it's shorter than the tool descriptions. python MAX TURNS = 5 def ask question : messages = {"role": "user", "content": {"text": question} } for turn in range MAX TURNS : response = bedrock.converse modelId=MODEL ID, messages=messages, toolConfig=TOOL CONFIG, reply = response "output" "message" messages.append reply if response "stopReason" = "tool use": return "\n".join b "text" for b in reply "content" if "text" in b results = for block in reply "content" : if "toolUse" not in block: continue use = block "toolUse" print f" calling {use 'name' } with {use 'input' }" results.append { "toolResult": { "toolUseId": use "toolUseId" , "content": {"json": run tool use "name" , use "input" } , } } messages.append {"role": "user", "content": results} return "Hit the turn limit without finishing." if name == " main ": print ask "What S3 buckets do I have, and how many objects are in each one?" Three things in there are worth reading twice. converse https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html is the Bedrock call for a back-and-forth conversation with a model, and toolConfig is how the tools come along for the ride. messages is the whole conversation, and you keep appending to it. The model has no memory between calls. That list is the memory. stopReason is the model telling you why it stopped talking. tool use means it wants something run https://docs.aws.amazon.com/bedrock/latest/userguide/tool-use-client-side.html . Anything else means it's answering you. The toolUseId matches a result to the request it answers, so send it back exactly as it arrived. The boto3 reference for converse https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-runtime/client/converse.html has the full shape. MAX TURNS is a rail, not a detail. A loop that can call a paid service forever is worth a hard stop. python agent.py What you should see: a calling list buckets with {} line, then a calling count objects line for each bucket you own, then a sentence listing your buckets with their counts. Read that order again, because it's the point of the whole article. The model asked for the names first, because it couldn't count a bucket without knowing what it was called. You didn't write that order anywhere. It came out of the loop. If you own a lot of buckets, the script may run out of turns and say so. That's the rail doing its job. Raise MAX TURNS once you've read the pricing page and decided you're happy. It works because of a split worth keeping for life: the model decides, and your code acts. The model never touched your AWS account. It asked for count objects with a bucket name, and your Python chose to run it. Every permission that mattered is still an IAM permission, short for Identity and Access Management, the AWS service that decides who can do what. Your agent can only do what the credentials running the script may do. So a tool that reads is a small decision, and a tool that deletes is a much bigger one. So where does a Bedrock knowledge base come in? Different job. A knowledge base looks answers up in your own documents and hands the text to the model as context. That's retrieval. What you built is action. Real systems use both, and the loop is the better one to learn first, because a knowledge base is mostly something you configure. And Agents for Amazon Bedrock is the managed version of the loop you just wrote by hand. Now it reads like a form instead of a mystery. REGION matches the Region where you asked for it, then check your credentials are allowed to call Bedrock. messages is the wrong shape. The usual cause is appending the tool results without the model's own reply in front of them. The conversation has to alternate, so reply goes in before the results do. Add a third tool tonight. Make it read-only, in any AWS service you're curious about, with a description that says plainly what it returns. Then ask a question that needs two of your three tools together, and watch which ones it reaches for. Which tool did you add, and did it pick the one you expected? Tell us in the comments. Every statement about how AWS behaves is linked to the AWS or boto3 documentation page it comes from, right where we make it. AWS changes things. If a step doesn't match your screen, the documentation is right and we're out of date. Tell us and we'll fix the article.