Serverless AI agents AWS Lambda Bedrock pair event-driven compute with managed model inference so you can ship an agent without provisioning a single server. This guide walks through a working deployment: a Lambda function triggered by S3 object creation that runs a reasoning loop against Bedrock, calls a DynamoDB tool, and persists the result. We use AWS SAM for reproducible infrastructure and Python 3.12 with boto3.
Step 1: Scaffold the SAM project
Create a working directory and initialize a minimal SAM app. I prefer the Python 3.12 runtime and the hello-world template as a base, then strip the boilerplate.
sam init --name bedrock-agent \
--runtime python3.12 \
--template hello-world \
--app-template hello-world \
--no-trace
cd bedrock-agent
Delete the default hello_world handler body and replace it with an agent package. Your layout should look like this:
bedrock-agent/
├── template.yaml
├── agent/
│ ├── __init__.py
│ ├── app.py
│ └── requirements.txt
└── events/
└── s3_put.json
In agent/requirements.txt, pin boto3 (already in Lambda runtime, but explicit is fine) and requests if you plan HTTP tools. For this walkthrough, only boto3 is needed.
Step 2: Define infrastructure in template.yaml
The SAM template declares the function, S3 trigger, DynamoDB table, and IAM permissions. Keep the function memory at 512 MB and timeout at 30 seconds; Bedrock calls are network-bound, not CPU-bound.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
AgentTable:
Type: AWS::Serverless::SimpleTable
Properties:
PrimaryKey:
Name: id
Type: String
BedrockAgentFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: agent/
Handler: app.lambda_handler
Runtime: python3.12
MemorySize: 512
Timeout: 30
Environment:
Variables:
TABLE_NAME: !Ref AgentTable
MODEL_ID: anthropic.claude-3-sonnet-20240229-v1:0
Policies:
- DynamoDBWritePolicy:
TableName: !Ref AgentTable
- Statement:
- Effect: Allow
Action: bedrock:InvokeModel
Resource: "*"
Events:
S3Put:
Type: S3
Properties:
Bucket: !Ref InputBucket
Events: s3:ObjectCreated:*
InputBucket:
Type: AWS::S3::Bucket
Permissions note
The bedrock:InvokeModel action is account-scoped; restrict the Resource to the model ARN in production. The wildcard above is for brevity in the lab.
Step 3: Implement the agent loop
The handler reads the S3 event, fetches the object body as the user task, then runs a fixed two-step ReAct loop. For a real system, cap iterations to avoid runaway cost.
import json
import boto3
import os
s3 = boto3.client('s3')
ddb = boto3.resource('dynamodb').Table(os.environ['TABLE_NAME'])
bedrock = boto3.client('bedrock-runtime')
MODEL_ID = os.environ['MODEL_ID']
def invoke_bedrock(prompt: str) -> str:
body = {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 512,
"messages": [{"role": "user", "content": prompt}]
}
resp = bedrock.invoke_model(
modelId=MODEL_ID,
body=json.dumps(body)
)
data = json.loads(resp['body'].read())
return data['content'][0]['text']
def lambda_handler(event, context):
record = event['Records'][0]
bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
obj = s3.get_object(Bucket=bucket, Key=key)
task = obj['Body'].read().decode('utf-8')
sys = "You are an agent. To use a tool, reply with JSON: {\"action\":\"query\",\"id\":\"123\"}."
first = invoke_bedrock(f"{sys}\nTask: {task}")
if '"action":"query"' in first:
# naive parse for demo
id_val = first.split('"id":"')[1].split('"')[0]
tool_result = ddb.get_item(Key={'id': id_val}).get('Item', {})
final = invoke_bedrock(f"Tool returned {json.dumps(tool_result)}. Complete task.")
else:
final = first
ddb.put_item(Item={'id': key, 'result': final[:4000]})
return {"status": "ok", "key": key}
This code is intentionally minimal. In production, use a proper JSON parser and a loop guard.
Step 4: Add a DynamoDB tool
The table created in Step 2 is the tool surface. The agent reads an item by id to ground its response. Seed an item locally:
aws dynamodb put-item \
--table-name BedrockAgentTable \
--item '{"id":{"S":"123"},"name":{"S":"test-record"}}'
The ddb.get_item call in app.py already handles the lookup. If you need write tools, add a second action type and map it to ddb.put_item.
Step 5: Deploy with SAM
Build and deploy. Use a unique S3 bucket for artifacts; SAM prompts for it on first guided deploy.
sam build
sam deploy --guided
When asked for Stack Name, use bedrock-agent-stack. Accept defaults for region and confirmation. After completion, note the InputBucket name from the outputs.
Step 6: Verify end-to-end
Create a task file and upload it to the bucket. The event file in events/s3_put.json mirrors the Lambda event structure for local testing, but a real upload triggers the function.
echo "Summarize the record with id 123" > task.txt
aws s3 cp task.txt s3://<InputBucket>/
Check CloudWatch Logs for the function:
aws logs tail /aws/lambda/BedrockAgentFunction --follow
Success criteria: a new item appears in the DynamoDB table with the S3 object key as id and a non-empty result attribute.
aws dynamodb get-item \
--table-name BedrockAgentTable \
--key '{"id":{"S":"task.txt"}}'
If the item exists and result contains model-generated text referencing the seeded record, the pipeline works.
Cold starts and concurrency
Serverless AI agents AWS Lambda Bedrock pay a cold-start tax only on the first invocation per worker. Bedrock itself is a network call, so keep the function warm with a provisioned concurrency of 1 if latency matters. For bursty workloads, set ReservedConcurrentExecutions to avoid exhausting Bedrock account quotas.
Error handling and idempotency
S3 triggers are at-least-once. Make the handler idempotent by using the object key as the DynamoDB primary key and relying on put-item overwrite. Wrap invoke_bedrock in a retry with exponential backoff for ThrottlingException. Bedrock returns 429 when the region’s capacity is saturated; a short sleep and retry is the correct reaction, not a failure.
When to move off Lambda
If your agent loop needs more than 15 minutes or large memory (e.g., in-process vector search), Lambda is the wrong host. For most event-driven, stateless agents, though, serverless AI agents AWS Lambda Bedrock are the cheapest path to production. Use Step Functions when the reasoning graph becomes a DAG rather than a loop.