AWS Builder Center
The Rise of AI Agents on AWS: From Chatbots to Autonomous Systems

The Rise of AI Agents on AWS: From Chatbots to Autonomous Systems

We’re entering an era where AI doesn’t just respond, but reasons, plans, and acts. Autonomous agents are here, and AWS is at the center.

AWS Community Builder - Dev Tools | 2× AWS Certified | Devops/Cloud Consultant

Introduction: The Agent Revolution

In 2022, the world marveled at ChatGPT’s ability to hold a conversation. By 2024, enterprises were deploying AI systems that could browse the web, write code, execute workflows, and coordinate with other AI agents, all without human intervention for each step.
This is the age of AI Agents.
On AWS, this transformation is accelerating through a rich constellation of services: Amazon Bedrock, AWS Lambda, Step Functions, Amazon Q, and purpose-built agent frameworks. Whether you’re a startup building a customer service bot or an enterprise deploying fully autonomous document processing pipelines, AWS offers a mature, production-ready stack.
This blog takes you from the foundational concepts all the way to real-world, production-grade autonomous agent architectures with working code at every step.

Part 1: Understanding AI Agents — What Makes Them Different?

From Chatbots to Agents

A chatbot operates in a stateless request-response loop. You ask, it answers. It has no memory of what it did 10 seconds ago, no ability to take action in the world, and no capacity to break a complex goal into sub-tasks.
An AI Agent is fundamentally different:

The Agent Anatomy: ReAct and Beyond

Most production agents today use the ReAct (Reasoning + Acting) loop, popularized in a 2022 Google paper. The agent cycles through:
  1. Observe: What is the current state of the world?
  2. Think: What should I do next to achieve my goal?
  3. Act: Execute a tool, API call, or compute step
  4. Reflect: Did that work? Should I adjust?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Goal: "Summarize last quarter's sales and email it to the team"

THINK: I need to find the sales data first.
ACT: query_database(table="sales", period="Q3-2024")
OBSERVE: [Returns 50,000 rows of data]

THINK: I'll aggregate this by product category.
ACT: run_python(code="df.groupby('category')['revenue'].sum()")
OBSERVE: [Returns summary table]

THINK: Now generate a human-readable summary.
ACT: call_llm(prompt="Summarize this sales data...")
OBSERVE: [Returns narrative text]

THINK: Send it via email.
ACT: send_email(to="team@company.com", body=summary)
OBSERVE: Email sent successfully.

DONE.
This loop is what differentiates an agent from a simple LLM call.

Part 2: The AWS AI Agent Stack

AWS has built a comprehensive set of services for every layer of the agent architecture.

Key Services Overview

Amazon Bedrock is the cornerstone of a fully managed service, giving access to leading foundation models (Claude, Llama 3, Mistral, Amazon Titan) with no infrastructure to manage.
Amazon Bedrock Agents adds the orchestration layer: tool definitions (Action Groups), knowledge retrieval (Knowledge Bases), memory management, and the ReAct loop, all managed.
AWS Lambda is the tool execution engine. Every agent action maps to a Lambda function.
AWS Step Functions enables complex, long-running, durable multi-agent workflows with visual modeling, error handling, and retry logic.
Amazon DynamoDB + ElastiCache power the agent’s short-term and long-term memory.

Part 3: Building Your First Agent on AWS

Prerequisites

1
2
3
4
5
6
# Install AWS CLI and configure
pip install awscli boto3
aws configure

# Install the AWS CDK for infrastructure
npm install -g aws-cdk

Step 1: Create an IAM Role for Bedrock Agents

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
# iam_setup.py
import boto3
import json

iam = boto3.client('iam')

trust_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "bedrock.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}

# Create the role
role = iam.create_role(
RoleName='BedrockAgentExecutionRole',
AssumeRolePolicyDocument=json.dumps(trust_policy),
Description='Execution role for Amazon Bedrock Agent'
)

# Attach necessary permissions
iam.attach_role_policy(
RoleName='BedrockAgentExecutionRole',
PolicyArn='arn:aws:iam::aws:policy/AmazonBedrockFullAccess'
)

print(f"Role ARN: {role['Role']['Arn']}")

Step 2: Define Your Agent’s Action Groups (Tools)

Action Groups are the tools your agent can call. Each action maps to a Lambda function and is described via an OpenAPI schema.
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# lambda_tool_handler.py — The tool executor

import json
import boto3
import requests
from datetime import datetime

def lambda_handler(event, context):
"""
Handles all tool calls from the Bedrock Agent.
The agent sends an event with actionGroup, function, and parameters.
"""

action_group = event.get('actionGroup')
function = event.get('function')
parameters = event.get('parameters', [])

# Convert parameters list to dict
params = {p['name']: p['value'] for p in parameters}

print(f"Agent calling: {action_group}/{function} with {params}")

if function == 'get_weather':
result = get_weather(params.get('city'))
elif function == 'search_database':
result = search_database(params.get('query'), params.get('limit', 10))
elif function == 'create_ticket':
result = create_support_ticket(params)
elif function == 'get_account_info':
result = get_account_info(params.get('account_id'))
else:
result = {"error": f"Unknown function: {function}"}

# CRITICAL: Response must follow this exact format for Bedrock Agents
return {
"messageVersion": "1.0",
"response": {
"actionGroup": action_group,
"function": function,
"functionResponse": {
"responseBody": {
"TEXT": {
"body": json.dumps(result)
}
}
}
}
}

def get_weather(city: str) -> dict:
"""Fetch weather for a city using a public API."""
# In production, use your weather API key
return {
"city": city,
"temperature": "22°C",
"condition": "Partly Cloudy",
"humidity": "65%",
"timestamp": datetime.utcnow().isoformat()
}

def search_database(query: str, limit: int) -> dict:
"""Search DynamoDB for relevant records."""
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('CustomerRecords')

# Simple scan with filter (use proper indexing in production)
response = table.scan(
FilterExpression='contains(#data, :query)',
ExpressionAttributeNames={'#data': 'searchable_text'},
ExpressionAttributeValues={':query': query},
Limit=limit
)

return {
"results": response.get('Items', []),
"count": len(response.get('Items', []))
}

def create_support_ticket(params: dict) -> dict:
"""Create a support ticket in the system."""
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('SupportTickets')

ticket_id = f"TKT-{datetime.utcnow().strftime('%Y%m%d%H%M%S')}"

table.put_item(Item={
'ticket_id': ticket_id,
'title': params.get('title'),
'description': params.get('description'),
'priority': params.get('priority', 'MEDIUM'),
'status': 'OPEN',
'created_at': datetime.utcnow().isoformat()
})

return {"ticket_id": ticket_id, "status": "OPEN", "message": "Ticket created successfully"}

def get_account_info(account_id: str) -> dict:
"""Retrieve account information."""
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Accounts')

response = table.get_item(Key={'account_id': account_id})
item = response.get('Item')

if not item:
return {"error": f"Account {account_id} not found"}

# Remove sensitive fields
item.pop('payment_info', None)
item.pop('ssn', None)

return item

Step 3: Deploy the Bedrock Agent via Python SDK

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# deploy_agent.py — Create and configure the Bedrock Agent

import boto3
import json
import time

bedrock_agent = boto3.client('bedrock-agent', region_name='us-east-1')

AGENT_ROLE_ARN = "arn:aws:iam::123456789012:role/BedrockAgentExecutionRole"
LAMBDA_ARN = "arn:aws:lambda:us-east-1:123456789012:function:AgentToolHandler"

def create_agent():
"""Create the Bedrock Agent."""
response = bedrock_agent.create_agent(
agentName='CustomerSupportAgent',
agentResourceRoleArn=AGENT_ROLE_ARN,
description='Autonomous customer support agent with tool use',

# Use Claude 3 Sonnet for the best balance of speed and intelligence
foundationModel='anthropic.claude-3-5-sonnet-20241022-v2:0',

instruction="""You are an expert customer support agent for Acme Corp.

Your capabilities:
- Look up customer account information
- Check weather for shipping estimates
- Search the knowledge base for solutions
- Create support tickets when issues can't be resolved immediately

Guidelines:
- Always verify the customer's account before providing sensitive information
- Be empathetic and professional
- Escalate to a human if the issue is complex or the customer is distressed
- Always provide a ticket number when creating tickets
- If you don't know something, say so — never guess

You have access to tools. Use them proactively to help customers efficiently."""
,

idleSessionTTLInSeconds=1800, # 30-minute session timeout
)

agent_id = response['agent']['agentId']
print(f"Created Agent ID: {agent_id}")
return agent_id

def create_action_group(agent_id: str):
"""Define the tools the agent can use."""

# OpenAPI schema defining available functions
api_schema = {
"openapi": "3.0.0",
"info": {"title": "Customer Support Tools", "version": "1.0.0"},
"paths": {
"/get_account_info": {
"get": {
"operationId": "get_account_info",
"description": "Retrieve customer account details by account ID",
"parameters": [
{
"name": "account_id",
"in": "query",
"required": True,
"schema": {"type": "string"},
"description": "The unique customer account identifier"
}
],
"responses": {"200": {"description": "Account information"}}
}
},
"/search_database": {
"get": {
"operationId": "search_database",
"description": "Search the customer database for relevant records",
"parameters": [
{
"name": "query",
"in": "query",
"required": True,
"schema": {"type": "string"}
},
{
"name": "limit",
"in": "query",
"required": False,
"schema": {"type": "integer", "default": 10}
}
],
"responses": {"200": {"description": "Search results"}}
}
},
"/create_ticket": {
"post": {
"operationId": "create_ticket",
"description": "Create a new support ticket for tracking an issue",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"description": {"type": "string"},
"priority": {
"type": "string",
"enum": ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
}
},
"required": ["title", "description"]
}
}
}
},
"responses": {"200": {"description": "Created ticket"}}
}
}
}
}

response = bedrock_agent.create_agent_action_group(
agentId=agent_id,
agentVersion='DRAFT',
actionGroupName='CustomerSupportActions',
description='Tools for customer support operations',
actionGroupExecutor={
'lambda': LAMBDA_ARN
},
apiSchema={
'payload': json.dumps(api_schema)
}
)

print(f"Created Action Group: {response['agentActionGroup']['actionGroupId']}")

def prepare_and_deploy(agent_id: str):
"""Prepare agent for testing and create an alias."""

# Prepare the agent (validate and compile)
bedrock_agent.prepare_agent(agentId=agent_id)
time.sleep(15) # Wait for preparation

# Create an alias for the agent version
alias = bedrock_agent.create_agent_alias(
agentId=agent_id,
agentAliasName='production',
description='Production deployment'
)

alias_id = alias['agentAlias']['agentAliasId']
print(f"Agent Alias ID: {alias_id}")
print(f"\nDeploy complete!")
print(f"Agent ID: {agent_id}")
print(f"Alias ID: {alias_id}")
return alias_id

# Run deployment
agent_id = create_agent()
create_action_group(agent_id)
alias_id = prepare_and_deploy(agent_id)

Step 4: Invoke Your Agent

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
55
56
57
58
59
60
61
62
63
64
65
# invoke_agent.py — Run the agent in conversation

import boto3
import uuid

bedrock_runtime = boto3.client('bedrock-agent-runtime', region_name='us-east-1')

AGENT_ID = "your-agent-id"
AGENT_ALIAS_ID = "your-alias-id"

def chat_with_agent(user_message: str, session_id: str = None) -> str:
"""
Send a message to the agent and get a response.
The agent will autonomously decide which tools to call.
"""

if not session_id:
session_id = str(uuid.uuid4())

response = bedrock_runtime.invoke_agent(
agentId=AGENT_ID,
agentAliasId=AGENT_ALIAS_ID,
sessionId=session_id,
inputText=user_message,
enableTrace=True # Enable for debugging
)

full_response = ""

# Process streaming response
for event in response['completion']:
if 'chunk' in event:
chunk = event['chunk']
full_response += chunk['bytes'].decode('utf-8')

# Trace shows the agent's reasoning (great for debugging)
if 'trace' in event:
trace = event['trace']['trace']
if 'orchestrationTrace' in trace:
orch = trace['orchestrationTrace']
if 'rationale' in orch:
print(f"Agent thinking: {orch['rationale']['text'][:100]}...")
if 'invocationInput' in orch:
inv = orch['invocationInput']
if 'actionGroupInvocationInput' in inv:
action = inv['actionGroupInvocationInput']
print(f"Calling tool: {action['function']}")

return full_response, session_id

# Example conversation
print("=== AI Agent Demo ===\n")

session = None

messages = [
"Hi! My account ID is ACC-12345. I'm having trouble with my last order.",
"The order was placed 3 days ago but I haven't received any tracking info.",
"Can you create a ticket for this? It's getting urgent."
]

for msg in messages:
print(f"User: {msg}")
response, session = chat_with_agent(msg, session)
print(f"Agent: {response}\n")
print("-" * 60)

Part 4: Adding RAG: Knowledge-Grounded Agents

Most enterprise agents need to answer questions from private knowledge bases (internal docs, product manuals, policy documents). This is where Retrieval Augmented Generation (RAG) enters.

Setting Up a Bedrock Knowledge Base

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# knowledge_base_setup.py

import boto3
import json

bedrock_agent = boto3.client('bedrock-agent', region_name='us-east-1')

def create_knowledge_base():
"""
Create a vector knowledge base backed by Amazon OpenSearch Serverless.
Documents in S3 are automatically chunked, embedded, and indexed.
"""


response = bedrock_agent.create_knowledge_base(
name='ProductKnowledgeBase',
description='Product documentation and support articles',
roleArn='arn:aws:iam::123456789012:role/BedrockKBRole',

knowledgeBaseConfiguration={
'type': 'VECTOR',
'vectorKnowledgeBaseConfiguration': {
# Use Amazon's Titan embedding model
'embeddingModelArn': 'arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-embed-text-v2:0'
}
},

storageConfiguration={
'type': 'OPENSEARCH_SERVERLESS',
'opensearchServerlessConfiguration': {
'collectionArn': 'arn:aws:aoss:us-east-1:123456789012:collection/my-kb-collection',
'vectorIndexName': 'bedrock-knowledge-base-default-index',
'fieldMapping': {
'vectorField': 'embedding',
'textField': 'AMAZON_BEDROCK_TEXT_CHUNK',
'metadataField': 'AMAZON_BEDROCK_METADATA'
}
}
}
)

kb_id = response['knowledgeBase']['knowledgeBaseId']
print(f"Knowledge Base ID: {kb_id}")
return kb_id

def create_data_source(kb_id: str, bucket_name: str):
"""Connect an S3 bucket as the document source."""

response = bedrock_agent.create_data_source(
knowledgeBaseId=kb_id,
name='ProductDocs-S3',
description='Product documentation from S3',

dataSourceConfiguration={
'type': 'S3',
's3Configuration': {
'bucketArn': f'arn:aws:s3:::{bucket_name}',
'inclusionPrefixes': ['docs/', 'articles/', 'manuals/']
}
},

vectorIngestionConfiguration={
'chunkingConfiguration': {
'chunkingStrategy': 'HIERARCHICAL',
'hierarchicalChunkingConfiguration': {
'levelConfigurations': [
{'maxTokens': 1500}, # Parent chunks
{'maxTokens': 300} # Child chunks (retrieved)
],
'overlapTokens': 60
}
}
}
)

ds_id = response['dataSource']['dataSourceId']
print(f"Data Source ID: {ds_id}")
return ds_id

def sync_knowledge_base(kb_id: str, ds_id: str):
"""Trigger ingestion — parse, chunk, embed, and index all documents."""

response = bedrock_agent.start_ingestion_job(
knowledgeBaseId=kb_id,
dataSourceId=ds_id
)

job_id = response['ingestionJob']['ingestionJobId']
print(f"Ingestion started. Job ID: {job_id}")
print("Monitor in the AWS Console or poll the ingestion job status.")
return job_id

# Direct KB query (without agent)
def query_knowledge_base(kb_id: str, question: str):
"""Query the KB directly for testing."""

bedrock_rt = boto3.client('bedrock-agent-runtime', region_name='us-east-1')

response = bedrock_rt.retrieve_and_generate(
input={'text': question},
retrieveAndGenerateConfiguration={
'type': 'KNOWLEDGE_BASE',
'knowledgeBaseConfiguration': {
'knowledgeBaseId': kb_id,
'modelArn': 'arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0',
'retrievalConfiguration': {
'vectorSearchConfiguration': {
'numberOfResults': 5,
'overrideSearchType': 'HYBRID' # Combines semantic + keyword search
}
}
}
}
)

return {
'answer': response['output']['text'],
'citations': response.get('citations', [])
}

Part 5: Multi-Agent Systems — The Next Frontier

Single agents have limits. Complex enterprise workflows require multi-agent systems where specialized agents collaborate.

Architecture: Multi-Agent Collaboration on AWS

Implementing Multi-Agent Collaboration with Bedrock

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# multi_agent_system.py
# Amazon Bedrock now supports native multi-agent collaboration

import boto3
import json
import uuid

bedrock_agent = boto3.client('bedrock-agent', region_name='us-east-1')
bedrock_rt = boto3.client('bedrock-agent-runtime', region_name='us-east-1')

# --- Step 1: Create Sub-Agents ---

def create_data_analyst_agent():
"""Specialist in querying and analyzing data."""
response = bedrock_agent.create_agent(
agentName='DataAnalystAgent',
agentResourceRoleArn='arn:aws:iam::123456789012:role/BedrockAgentRole',
foundationModel='anthropic.claude-3-5-sonnet-20241022-v2:0',
instruction="""You are a data analyst specialist.
Your job is to query databases, run SQL, analyze datasets, and return
structured results. Always return data in JSON format.
Be precise and focus only on data analysis tasks."""

)
return response['agent']['agentId']

def create_code_writer_agent():
"""Specialist in writing and executing code."""
response = bedrock_agent.create_agent(
agentName='CodeWriterAgent',
agentResourceRoleArn='arn:aws:iam::123456789012:role/BedrockAgentRole',
foundationModel='anthropic.claude-3-5-sonnet-20241022-v2:0',
instruction="""You are a Python expert and software engineer.
Write clean, well-documented code. Always include error handling.
When asked to analyze data, write efficient Pandas/NumPy code.
Return code results in structured JSON."""

)
return response['agent']['agentId']

# --- Step 2: Create Supervisor Agent with Sub-Agent Collaboration ---

def create_supervisor_agent(analyst_agent_alias_arn: str, code_agent_alias_arn: str):
"""Orchestrator that delegates to specialist sub-agents."""

response = bedrock_agent.create_agent(
agentName='SupervisorAgent',
agentResourceRoleArn='arn:aws:iam::123456789012:role/BedrockAgentRole',
foundationModel='anthropic.claude-3-5-sonnet-20241022-v2:0',
instruction="""You are a business intelligence supervisor.

When given a complex task, break it down and delegate to specialists:
- For data queries and analysis → use the DataAnalystAgent
- For code writing and execution → use the CodeWriterAgent

Synthesize results from sub-agents into a clear, actionable response.
Always cite which sub-agent produced which insights."""
,

# Enable multi-agent collaboration
agentCollaboration='SUPERVISOR'
)

supervisor_id = response['agent']['agentId']

# Associate sub-agents with the supervisor
bedrock_agent.associate_agent_collaborator(
agentId=supervisor_id,
agentVersion='DRAFT',
agentDescriptor={'aliasArn': analyst_agent_alias_arn},
collaboratorName='DataAnalystAgent',
collaborationInstruction='Use for data queries, SQL, and statistical analysis tasks.'
)

bedrock_agent.associate_agent_collaborator(
agentId=supervisor_id,
agentVersion='DRAFT',
agentDescriptor={'aliasArn': code_agent_alias_arn},
collaboratorName='CodeWriterAgent',
collaborationInstruction='Use for writing Python code, data processing scripts, and automation.'
)

return supervisor_id

# --- Step 3: Invoke the Multi-Agent System ---

def run_multi_agent_task(task: str, supervisor_id: str, alias_id: str):
"""Send a complex task to the supervisor agent system."""

session_id = str(uuid.uuid4())

response = bedrock_rt.invoke_agent(
agentId=supervisor_id,
agentAliasId=alias_id,
sessionId=session_id,
inputText=task,
enableTrace=True
)

full_response = ""
agent_calls = []

for event in response['completion']:
if 'chunk' in event:
full_response += event['chunk']['bytes'].decode('utf-8')

if 'trace' in event:
trace = event['trace']['trace']
# Track sub-agent invocations
if 'orchestrationTrace' in trace:
orch = trace['orchestrationTrace']
if 'invocationInput' in orch:
inv = orch['invocationInput']
if 'agentCollaboratorInvocationInput' in inv:
collab = inv['agentCollaboratorInvocationInput']
agent_calls.append(collab.get('agentCollaboratorName'))
print(f"Delegating to: {collab.get('agentCollaboratorName')}")

return full_response, agent_calls

# Example usage
task = """
Analyze our Q4 2024 sales data:
1. Find the top 5 products by revenue
2. Write a Python script to generate a trend chart
3. Identify any anomalies in the data
Provide a summary suitable for an executive presentation.
"""


result, agents_used = run_multi_agent_task(task, "supervisor-id", "alias-id")
print(f"\nResult: {result}")
print(f"Sub-agents used: {agents_used}")

Part 6: Durable Workflows AWS Step Functions + AI Agents

For long-running, mission-critical agent workflows (document processing, financial pipelines, compliance checks), AWS Step Functions provides durability, observability, and error recovery.

Document Processing Pipeline

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
{
"Comment": "AI-powered document processing with Step Functions",
"StartAt": "ValidateDocument",
"States": {
"ValidateDocument": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ValidateDocument",
"ResultPath": "$.validation",
"Next": "IsDocumentValid"
},
"IsDocumentValid": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.validation.isValid",
"BooleanEquals": true,
"Next": "InvokeAIAgent"
}
],
"Default": "HandleInvalidDocument"
},
"InvokeAIAgent": {
"Type": "Task",
"Resource": "arn:aws:states:::bedrock:invokeAgent",
"Parameters": {
"agentId": "your-agent-id",
"agentAliasId": "your-alias-id",
"sessionId.$": "$.sessionId",
"inputText.$": "States.Format('Process this document and extract key information: {}', $.documentContent)"
},
"ResultPath": "$.agentResponse",
"Retry": [
{
"ErrorEquals": ["ThrottlingException", "ServiceUnavailableException"],
"IntervalSeconds": 5,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"Next": "HandleAgentError",
"ResultPath": "$.error"
}
],
"Next": "StoreResults"
},
"StoreResults": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:putItem",
"Parameters": {
"TableName": "ProcessedDocuments",
"Item": {
"document_id": {"S.$": "$.documentId"},
"agent_output": {"S.$": "$.agentResponse.output.text"},
"processed_at": {"S.$": "$$.Execution.StartTime"},
"status": {"S": "COMPLETED"}
}
},
"Next": "NotifySuccess"
},
"NotifySuccess": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn": "arn:aws:sns:us-east-1:123456789012:DocumentProcessingAlerts",
"Message.$": "States.Format('Document {} processed successfully', $.documentId)"
},
"End": true
},
"HandleAgentError": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:HandleError",
"End": true
},
"HandleInvalidDocument": {
"Type": "Fail",
"Error": "InvalidDocument",
"Cause": "Document failed validation checks"
}
}
}

The Lambda Functions for the Pipeline

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# pipeline_lambdas.py

import boto3
import json
import re
from datetime import datetime

def validate_document(event, context):
"""Validate incoming document before AI processing."""

document_id = event.get('documentId')
document_content = event.get('documentContent', '')
document_type = event.get('documentType')

issues = []

# Basic validation checks
if not document_content:
issues.append("Document content is empty")

if len(document_content) > 100000: # 100KB text limit
issues.append("Document exceeds maximum size")

if document_type not in ['INVOICE', 'CONTRACT', 'REPORT', 'EMAIL']:
issues.append(f"Unsupported document type: {document_type}")

# Sensitive data detection (simplified)
if re.search(r'\b\d{9}\b', document_content): # SSN pattern
issues.append("Document may contain SSN — flagged for review")

return {
'isValid': len(issues) == 0,
'issues': issues,
'documentId': document_id,
'contentLength': len(document_content),
'validatedAt': datetime.utcnow().isoformat()
}

def handle_error(event, context):
"""Log errors and trigger appropriate recovery actions."""

error = event.get('error', {})
document_id = event.get('documentId')

# Log to CloudWatch
print(json.dumps({
'level': 'ERROR',
'documentId': document_id,
'error': error,
'timestamp': datetime.utcnow().isoformat()
}))

# Store failed record in DynamoDB for retry
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('FailedDocuments')

table.put_item(Item={
'document_id': document_id,
'error_message': str(error),
'failed_at': datetime.utcnow().isoformat(),
'retry_count': 0,
'status': 'FAILED'
})

# Alert via SNS if critical
if 'ThrottlingException' not in str(error):
sns = boto3.client('sns')
sns.publish(
TopicArn='arn:aws:sns:us-east-1:123456789012:CriticalAlerts',
Subject=f'Document Processing Failed: {document_id}',
Message=json.dumps(event, indent=2)
)

return {'status': 'ERROR_HANDLED', 'documentId': document_id}

Part 7: Observability and Production Best Practices

Running AI agents in production demands comprehensive observability.

CloudWatch Metrics and Alarms

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# observability.py — Production monitoring for AI agents

import boto3
import json
from datetime import datetime, timedelta

cloudwatch = boto3.client('cloudwatch')

def publish_agent_metrics(agent_id: str, session_id: str, metrics: dict):
"""Publish custom agent performance metrics to CloudWatch."""

cloudwatch.put_metric_data(
Namespace='AIAgents/Bedrock',
MetricData=[
{
'MetricName': 'AgentLatencyMs',
'Dimensions': [
{'Name': 'AgentId', 'Value': agent_id},
{'Name': 'Environment', 'Value': 'production'}
],
'Value': metrics['latency_ms'],
'Unit': 'Milliseconds',
'Timestamp': datetime.utcnow()
},
{
'MetricName': 'ToolInvocations',
'Dimensions': [
{'Name': 'AgentId', 'Value': agent_id},
{'Name': 'ToolName', 'Value': metrics.get('tool_name', 'unknown')}
],
'Value': metrics.get('tool_count', 1),
'Unit': 'Count',
'Timestamp': datetime.utcnow()
},
{
'MetricName': 'InputTokens',
'Dimensions': [{'Name': 'AgentId', 'Value': agent_id}],
'Value': metrics.get('input_tokens', 0),
'Unit': 'Count',
'Timestamp': datetime.utcnow()
},
{
'MetricName': 'OutputTokens',
'Dimensions': [{'Name': 'AgentId', 'Value': agent_id}],
'Value': metrics.get('output_tokens', 0),
'Unit': 'Count',
'Timestamp': datetime.utcnow()
}
]
)

def create_agent_alarms(agent_id: str, sns_topic_arn: str):
"""Create CloudWatch alarms for agent health monitoring."""

alarms = [
{
'AlarmName': f'AgentHighLatency-{agent_id}',
'AlarmDescription': 'Agent P95 latency exceeds 30 seconds',
'MetricName': 'AgentLatencyMs',
'Statistic': 'p95',
'Threshold': 30000,
'ComparisonOperator': 'GreaterThanThreshold',
'EvaluationPeriods': 3,
'Period': 300, # 5 minutes
},
{
'AlarmName': f'AgentHighToolFailure-{agent_id}',
'AlarmDescription': 'Tool invocation failure rate > 5%',
'MetricName': 'ToolInvocationErrors',
'Statistic': 'Sum',
'Threshold': 10,
'ComparisonOperator': 'GreaterThanThreshold',
'EvaluationPeriods': 1,
'Period': 300,
}
]

for alarm in alarms:
cloudwatch.put_metric_alarm(
**alarm,
Namespace='AIAgents/Bedrock',
Dimensions=[{'Name': 'AgentId', 'Value': agent_id}],
AlarmActions=[sns_topic_arn],
OKActions=[sns_topic_arn],
TreatMissingData='notBreaching'
)
print(f"Created alarm: {alarm['AlarmName']}")

def get_agent_dashboard_data(agent_id: str, hours: int = 24):
"""Retrieve agent performance metrics for a dashboard."""

end_time = datetime.utcnow()
start_time = end_time - timedelta(hours=hours)

metrics_to_fetch = ['AgentLatencyMs', 'ToolInvocations', 'InputTokens', 'OutputTokens']
dashboard_data = {}

for metric_name in metrics_to_fetch:
response = cloudwatch.get_metric_statistics(
Namespace='AIAgents/Bedrock',
MetricName=metric_name,
Dimensions=[{'Name': 'AgentId', 'Value': agent_id}],
StartTime=start_time,
EndTime=end_time,
Period=3600, # 1-hour buckets
Statistics=['Average', 'Sum', 'Maximum']
)

dashboard_data[metric_name] = sorted(
response['Datapoints'],
key=lambda x: x['Timestamp']
)

return dashboard_data

Cost Optimization Strategies

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
# cost_optimizer.py — Keep your agent costs under control

import boto3

def implement_token_caching(system_prompt: str):
"""
Use prompt caching to reduce costs for repeated system prompts.
Caching can reduce input token costs by up to 90% for cached content.
"""


bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')

response = bedrock.invoke_model(
modelId='anthropic.claude-3-5-sonnet-20241022-v2:0',
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1000,
"system": [
{
"type": "text",
"text": system_prompt,
"cache_control": {"type": "ephemeral"} # Cache this expensive prompt
}
],
"messages": [
{"role": "user", "content": "What can you help me with?"}
]
}),
contentType='application/json',
accept='application/json'
)

return json.loads(response['body'].read())

def select_model_by_complexity(task: str, task_complexity: str) -> str:
"""Route tasks to the right model based on complexity — save up to 80% on costs."""

model_map = {
'simple': 'anthropic.claude-3-haiku-20240307-v1:0', # Fast, cheap
'medium': 'anthropic.claude-3-5-sonnet-20241022-v2:0', # Balanced
'complex': 'anthropic.claude-3-5-sonnet-20241022-v2:0', # Most capable
}

# Route based on complexity hints
if any(word in task.lower() for word in ['summarize', 'classify', 'extract']):
return model_map['simple']
elif any(word in task.lower() for word in ['analyze', 'compare', 'explain']):
return model_map['medium']
else:
return model_map.get(task_complexity, model_map['medium'])

Part 8: Real-World Architecture Enterprise Document Intelligence Agent

Let’s bring everything together into a production-ready architecture.

Part 9: Security Guardrails for Production Agents

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# guardrails.py — Safety controls for AI agents in production

import boto3

bedrock = boto3.client('bedrock', region_name='us-east-1')

def create_agent_guardrail():
"""
Create comprehensive guardrails to prevent harmful outputs,
filter sensitive data, and enforce topic restrictions.
"""


response = bedrock.create_guardrail(
name='ProductionAgentGuardrail',
description='Safety controls for the customer support agent',

# Block harmful topics
topicPolicyConfig={
'topicsConfig': [
{
'name': 'CompetitorComparisons',
'definition': 'Questions asking to compare our products unfavorably with competitors',
'examples': ['Why is Competitor X better than you?'],
'type': 'DENY'
},
{
'name': 'LegalAdvice',
'definition': 'Requests for legal advice or legal interpretations',
'examples': ['Can I sue you for this?'],
'type': 'DENY'
}
]
},

# Detect and redact sensitive data
sensitiveInformationPolicyConfig={
'piiEntitiesConfig': [
{'type': 'CREDIT_DEBIT_CARD_NUMBER', 'action': 'BLOCK'},
{'type': 'US_SOCIAL_SECURITY_NUMBER', 'action': 'BLOCK'},
{'type': 'EMAIL', 'action': 'ANONYMIZE'},
{'type': 'PHONE', 'action': 'ANONYMIZE'},
{'type': 'NAME', 'action': 'ANONYMIZE'},
],
'regexesConfig': [
{
'name': 'InternalAccountNumbers',
'pattern': 'ACCT-[0-9]{8}',
'action': 'ANONYMIZE',
'description': 'Internal account number format'
}
]
},

# Content filtering
contentPolicyConfig={
'filtersConfig': [
{'type': 'SEXUAL', 'inputStrength': 'HIGH', 'outputStrength': 'HIGH'},
{'type': 'VIOLENCE', 'inputStrength': 'MEDIUM', 'outputStrength': 'HIGH'},
{'type': 'HATE', 'inputStrength': 'HIGH', 'outputStrength': 'HIGH'},
{'type': 'INSULTS', 'inputStrength': 'MEDIUM', 'outputStrength': 'HIGH'},
{'type': 'MISCONDUCT', 'inputStrength': 'HIGH', 'outputStrength': 'HIGH'},
{'type': 'PROMPT_ATTACK', 'inputStrength': 'HIGH', 'outputStrength': 'NONE'},
]
},

# Grounding — prevent hallucination
groundingPolicyConfig={
'filtersConfig': [
{
'type': 'GROUNDING',
'threshold': 0.75 # Block responses less than 75% grounded in source material
},
{
'type': 'RELEVANCE',
'threshold': 0.75
}
]
},

blockedInputMessaging='I cannot process this request. Please contact human support.',
blockedOutputsMessaging='I cannot provide that information. A support agent will assist you.'
)

guardrail_id = response['guardrailId']
print(f"Guardrail created: {guardrail_id}")
return guardrail_id

Conclusion: The Autonomous Future is Here

The trajectory is clear: AI systems on AWS are rapidly moving from reactive chatbots to proactive autonomous agents capable of reasoning, planning, and executing complex multi-step workflows.

The AWS AI Agent Maturity Model

1
2
3
4
5
Level 1: Chatbot → Single-turn Q&A, no tools
Level 2: Assistant → Multi-turn conversation, basic memory
Level 3: Tool-Using Agent → Calls APIs, reads/writes data, uses RAG
Level 4: Autonomous Agent → Plans, multi-step execution, self-corrects
Level 5: Multi-Agent → Specialized agents collaborate on complex tasks
Most enterprises today are between Level 2 and 3. The tooling — Bedrock Agents, Step Functions, Knowledge Bases, Guardrails — now makes Levels 4 and 5 achievable without a research team.

Key Takeaways

Start with Bedrock Agents for managed orchestration. Don’t build your own ReAct loop unless you have very specific requirements.
Use Knowledge Bases to ground your agents in private data. Hybrid search (semantic + keyword) dramatically improves retrieval quality.
Step Functions are essential for any workflow that needs durability, error handling, or runs longer than 15 minutes.
Guardrails are not optional in production. Deploy them from day one.
Multi-agent architectures are the right pattern for complex, multi-domain problems. Build specialist agents first, then orchestrate.
Observe everything. Token usage, latency, tool invocations, and failure rates should all be in CloudWatch from the start.

Resources

If this article helped you, give it a like, comments and share it with your team. The autonomous AI era is just beginning — and AWS is building the infrastructure to make it production-ready.the  
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article