# How to Build an AI Agent with AWS Bedrock

- Tool: AWS Bedrock AgentCore
- Difficulty: Advanced
- Time required: 45-60 min
- Compatibility: Python 3.10+, AWS account with Bedrock model access, Node.js 20+ for the agentcore CLI, macOS/Linux/WSL
- Last updated: November 2026

## TL;DR

Bedrock AgentCore is AWS's hosting layer for any agent framework, not a framework itself. This guide builds a shipment-support agent with the Strands Agents SDK, wraps it in BedrockAgentCoreApp, and deploys it with the agentcore CLI. One tool reads shipment status freely; a second issues a shipping credit only when called with an approved flag a human sets after reviewing the first response. Budget 45-60 minutes and an AWS account with Bedrock model access enabled.

## What you'll build

You'll build a shipment-support agent for an e-commerce backend. It can look up a shipment's status freely, and when a delay warrants a goodwill credit, it drafts the amount and reason but only issues it when the request carries approved: true. In practice that means the first call back to the customer gets a drafted amount and 'a support lead will confirm this,' and the actual credit only goes out on a second invoke made after that lead says yes, outside the conversation entirely.

Bedrock AgentCore doesn't care which framework wrote your agent; it's a Runtime, a Gateway for turning existing APIs into MCP tools, Memory, Identity, and Observability sitting underneath whatever agent code you bring. This guide uses Strands, AWS's own agent SDK, because it pairs natively with AgentCore, but the same BedrockAgentCoreApp wrapper works around a LangGraph, CrewAI, or OpenAI Agents SDK agent just as well. The only AWS-specific code is the wrapper itself: a few lines that turn your existing agent into something the Runtime can invoke.

Choose AgentCore when your agent already runs somewhere on AWS, or needs to, and you want a managed runtime, credential handling, and observability instead of standing up your own container infrastructure. If you're not on AWS, the Claude Agent SDK guide builds the same approval boundary as a process you run and host yourself.

## Before you start

- Python 3.10+ and an AWS account with access to a Claude model enabled in the Bedrock console's Model access page
- AWS credentials configured locally (aws configure) with permission to create AgentCore resources
- Node.js 20+ for the agentcore CLI, and uv for the Python agent build step
- A shipments API endpoint to call (or a mock one for testing)

## Step-by-step guide

### 1. Install the SDK, the CLI, and Strands

Install the Bedrock AgentCore Python package, the Strands Agents SDK, and the agentcore CLI, which is distributed through npm rather than pip since it's a standalone deployment tool, not a Python library. Confirm you have at least one Claude model enabled under Bedrock's Model access page in your account's region before continuing; AgentCore Runtime won't help if the underlying model call is denied.

```
pip install bedrock-agentcore strands-agents boto3
npm install -g @aws/agentcore
aws configure   # set your AWS access key, secret, and region
```

**Expected result:** agentcore --version prints a version, and python -c "import strands" runs with no error.

### 2. Define the read-only tool: get_shipment_status

Write get_shipment_status with Strands' @tool decorator. Its docstring is what the model reads to decide when to call it, so describe exactly what it returns. This tool only reads from your shipments API; keep any write logic entirely out of it, the same separation of concerns that makes the next tool's approval gate meaningful.

```
from strands import Agent, tool
import httpx

@tool
def get_shipment_status(shipment_id: str) -> dict:
    """Look up a shipment's current status, carrier, and expected delivery date."""
    resp = httpx.get(f"https://api.example-store.com/shipments/{shipment_id}")
    resp.raise_for_status()
    return resp.json()
```

**Expected result:** A plain Python function decorated as a Strands tool, callable on its own for a quick test against your shipments API.

### 3. Define the gated tool: issue_shipping_credit

issue_shipping_credit takes an approved flag that defaults to False. When it's False, the tool returns a drafted amount and reason without charging anything; when a human has reviewed the draft and the next invoke passes approved=True, it actually calls your payments API. The function itself enforces the gate, so no amount of clever prompting in the conversation can skip it.

```
@tool
def issue_shipping_credit(shipment_id: str, amount: float, reason: str, approved: bool = False) -> dict:
    """Draft or issue a shipping credit. Only issues the credit when approved is True;
    otherwise returns a draft for a human to review."""
    if not approved:
        return {"status": "drafted", "amount": amount, "reason": reason,
                "note": "A support lead must confirm this before it's issued."}
    # In production this calls your payments API (e.g. Stripe credit notes)
    return {"status": "issued", "amount": amount, "reason": reason}
```

**Expected result:** Calling the function directly with approved=False returns a draft; with approved=True it returns an issued result.

### 4. Build the Strands agent

Wrap both tools in a Strands Agent with a system prompt that states the rule in plain language: always check status before promising anything, and never tell the customer a credit is confirmed unless the tool actually returned status issued. Strands talks to Bedrock directly; leave model unset to use your account's available Claude model, or pass model with the exact Bedrock model ID shown on your Model access page if you need to pin one.

```
SYSTEM_PROMPT = (
    "You are a shipment support agent. Always call get_shipment_status before "
    "discussing a shipment. If a delay justifies a credit, call issue_shipping_credit "
    "with approved left as False and tell the customer a support lead will confirm it. "
    "Never say a credit was issued unless the tool result says status: issued."
)

agent = Agent(
    system_prompt=SYSTEM_PROMPT,
    tools=[get_shipment_status, issue_shipping_credit],
)
```

**Expected result:** agent("My order SHIP-204 is late, can I get something for the delay?") returns a drafted credit with no actual charge made.

### 5. Wrap it for AgentCore Runtime

BedrockAgentCoreApp is the only AWS-specific code in this guide. Its @app.entrypoint decorator marks the function AgentCore Runtime calls on every invoke; read the prompt and the approved flag out of the incoming request and pass them through to the Strands agent. app.run() starts the server AgentCore's Runtime expects.

```
from bedrock_agentcore import BedrockAgentCoreApp

app = BedrockAgentCoreApp()

@app.entrypoint
def handler(request):
    prompt = request.get("prompt", "")
    approved = request.get("approved", False)
    if approved:
        prompt += " The pending credit has been approved by a support lead; issue it now."
    result = agent(prompt)
    return {"result": str(result)}

app.run()
```

**Expected result:** Running the file locally with python agent.py starts a local server that AgentCore's dev mode can hit for testing.

### 6. Deploy with the agentcore CLI

agentcore create scaffolds the project configuration around your existing file, agentcore build packages it, and agentcore deploy ships it to AgentCore Runtime. Use agentcore dev first if you want to test against a browser-based Agent Inspector before deploying for real.

```
agentcore create --name shipment-support --template agent-python-strands
agentcore dev --mode browser   # optional local test with Agent Inspector
agentcore build
agentcore deploy
```

**Expected result:** agentcore deploy finishes with a Runtime ARN and status Active, visible again from agentcore status.

### 7. Invoke it twice: once drafted, once approved

Call the deployed agent with approved left out (or false) and confirm you get a drafted credit, not an issued one. Then simulate the human review step, a support lead reading the draft in your own ticketing tool, and send a second invoke with approved: true for the same shipment and amount. Only this second call should show status: issued in the tool's output.

```
agentcore invoke --runtime shipment-support --payload '{"prompt": "SHIP-204 is 3 days late, can I get a credit?"}'
# ... support lead reviews the drafted amount ...
agentcore invoke --runtime shipment-support --payload '{"prompt": "Issue the credit for SHIP-204", "approved": true}'
```

**Expected result:** The first invoke's response shows a drafted credit; the second, approved invoke shows the credit as issued.

## Complete code example

File: `agent.py`

```python
"""Shipment support agent: looks up shipment status freely, issues a credit
only on a second, explicitly approved invoke.
Run: python agent.py (pip install bedrock-agentcore strands-agents httpx)
"""
import httpx
from strands import Agent, tool
from bedrock_agentcore import BedrockAgentCoreApp


@tool
def get_shipment_status(shipment_id: str) -> dict:
    """Look up a shipment's current status, carrier, and expected delivery date."""
    resp = httpx.get(f"https://api.example-store.com/shipments/{shipment_id}", timeout=10)
    resp.raise_for_status()
    return resp.json()


@tool
def issue_shipping_credit(shipment_id: str, amount: float, reason: str, approved: bool = False) -> dict:
    """Draft or issue a shipping credit for a delayed shipment.

    Only actually issues the credit when approved is True. Otherwise returns
    a draft a human support lead must confirm first.
    """
    if not approved:
        return {
            "status": "drafted",
            "shipment_id": shipment_id,
            "amount": amount,
            "reason": reason,
            "note": "A support lead must confirm this before it's issued.",
        }
    # In production this calls your payments API (e.g. Stripe credit notes)
    return {"status": "issued", "shipment_id": shipment_id, "amount": amount, "reason": reason}


SYSTEM_PROMPT = (
    "You are a shipment support agent. Always call get_shipment_status before "
    "discussing any shipment. If a delay justifies a credit, call "
    "issue_shipping_credit with approved left as False, and tell the customer a "
    "support lead will confirm it. Never tell a customer a credit was issued "
    "unless the tool result's status is 'issued'."
)

agent = Agent(
    system_prompt=SYSTEM_PROMPT,
    tools=[get_shipment_status, issue_shipping_credit],
)

app = BedrockAgentCoreApp()


@app.entrypoint
def handler(request: dict) -> dict:
    prompt = request.get("prompt", "")
    approved = request.get("approved", False)
    if approved:
        prompt += (
            " The pending credit for this shipment has just been approved by a "
            "support lead; call issue_shipping_credit again with approved=true and issue it."
        )
    result = agent(prompt)
    return {"result": str(result)}


if __name__ == "__main__":
    app.run()
```

## Common mistakes

- **Letting the model set approved=True on its own because the conversation 'sounds confident'** — undefined Fix: approved should only ever come from your own entrypoint logic after a real external confirmation, never from the model's own judgment inside a single conversation.
- **Deploying before checking Bedrock Model access for the Claude model your agent needs** — undefined Fix: A model that isn't enabled in your account's region fails at invoke time with an access error that has nothing to do with your agent code. Check Model access first.
- **Putting write logic and read logic in the same tool function** — undefined Fix: Keep get_shipment_status read-only and issue_shipping_credit as the only function that can ever change a real record. Mixing them removes the one clean boundary the approval gate depends on.
- **Hardcoding the shipments API's credentials in the tool function instead of using AgentCore Identity** — undefined Fix: AgentCore Identity exists specifically so tools authenticate to downstream APIs without embedding static secrets in your deployed code.
- **Assuming the agentcore CLI works the same as the older bedrock-agentcore-starter-toolkit package** — undefined Fix: The starter toolkit is deprecated; install the current agentcore CLI via npm install -g @aws/agentcore and use its create/build/deploy/invoke commands instead of the old configure/launch verbs.

## Best practices

- Keep every tool that can change a real record separate from tools that only read, and gate the write tool's own function on an explicit flag your deployment code controls.
- Use AgentCore Identity for downstream API credentials instead of environment variables baked into the deployed package.
- Use AgentCore Memory if the same customer's shipment conversation needs to span more than one invoke; don't rely on the model remembering across separate Runtime calls.
- Route AgentCore's observability data into CloudWatch and alert on repeated drafted-but-never-approved credits; that pattern usually means the approval step isn't being followed up on.
- State the exact tool-result field the model must check (status: issued) in the system prompt, not a vague instruction like 'only say it's done if it's really done.'
- Test agentcore dev locally with the Agent Inspector before every deploy; catching a bad tool call locally is much cheaper than debugging it against the live Runtime.

## Use cases

### undefined



### undefined



### undefined



## Frequently asked questions

### Do I have to use Strands to deploy an agent to Bedrock AgentCore?

No. AgentCore Runtime is framework-agnostic; it hosts agents built with LangChain, LangGraph, CrewAI, the OpenAI Agents SDK, or the Claude Agent SDK the same way, through the same BedrockAgentCoreApp wrapper. This guide uses Strands because it's AWS's own agent SDK and pairs natively with AgentCore.

### What's the difference between Bedrock AgentCore and the older Bedrock Agents?

Bedrock Agents (now called Agents Classic) has been in maintenance mode and closed to new customers since July 30, 2026. AgentCore is the current platform: a Runtime, Gateway, Memory, Identity, and Observability layer that works with any framework instead of Bedrock's own built-in agent format.

### Why does the agentcore CLI install through npm instead of pip?

The current agentcore CLI (package @aws/agentcore) is a standalone deployment tool distributed via npm, separate from the bedrock-agentcore Python SDK your agent code imports. The older bedrock-agentcore-starter-toolkit pip package that used to provide a CLI is now deprecated in favor of it.

### Can AgentCore Gateway replace the direct HTTP call in get_shipment_status?

Yes. Gateway turns an existing API or Lambda function into an MCP tool with centralized auth, so instead of calling httpx directly inside the tool you'd call it through Gateway and get permission checks and logging for free. This guide calls the API directly to keep the first deploy simple.

### What does running this cost?

You pay standard Bedrock model token pricing for each agent call, plus AgentCore Runtime at roughly $0.09/vCPU-hour and $0.0095/GB-hour of compute while the agent is active, metered per invoke rather than as a flat monthly fee.

### How do I know the approval gate actually can't be bypassed from inside the conversation?

Because the check lives in the issue_shipping_credit function itself, not in the system prompt. A customer or a cleverly worded message can influence what the model says, but it can't flip the approved parameter your own entrypoint code controls.

### Can someone build this for me instead of deploying it myself?

Yes. RapidDev builds production agents on AWS Bedrock AgentCore, the Claude Agent SDK, and LangGraph, including the approval gating and observability wiring most tutorials skip. A free 30-minute consultation is the fastest way to scope your version.

---

Source: https://www.rapidevelopers.com/build-ai-agent/how-to-build-an-ai-agent-with-aws-bedrock
© RapidDev — https://www.rapidevelopers.com/build-ai-agent/how-to-build-an-ai-agent-with-aws-bedrock
