Serverless Product Feedback Analyzer
Build an AI pipeline that categorizes customer feedback using AWS and Claude.
Introduction
30 Second Summary
During the FIFA World Cup, millions of fans flood apps and social media with reactions about matches, referees, players, and stadiums. The real challenge is not collecting all those opinions. It is turning thousands of raw comments into a clear picture of what fans actually care about.
In this project, you will build a serverless pipeline that automatically categorizes World Cup fan feedback using AI. You will upload a CSV of fan comments to the cloud, and an AI model will extract structured insights like theme, sentiment, severity, and type for each item.
What You'll Build
A serverless product feedback analyzer that uses AWS Lambda, Amazon S3, DynamoDB, and Amazon Bedrock to process raw customer feedback through Claude and store prioritized insights.
By the end of this project, you'll have:
- A working event-driven pipeline that automatically triggers AI analysis when you upload a feedback file to S3.
- Structured feedback insights (theme, sentiment, severity, type) stored in DynamoDB and queryable via Python.
- Hands-on experience wiring together 5 core AWS services (Lambda, S3, DynamoDB, Bedrock, IAM) in a real AI engineering workflow.
- Secret Mission: a prioritization report generator that groups feedback by theme and ranks feature requests by demand.
Are there any prerequisites?
You need basic Python knowledge (functions, loops, imports). No AWS experience is required. This project walks you through every service from scratch.
Set Up Your Environment
Before you can build your serverless feedback analyzer, you need the right tools installed on your Mac.
In this step, you will install Python and VS Code locally, enable access to Claude through Amazon Bedrock, and verify that your AWS CLI is connected to your IAM user.
In this step, get ready to:
- Install Python 3.13 and VS Code.
- Enable Bedrock model access for Claude.
- Verify the AWS CLI is connected to your IAM user.
Install Python and VS Code
Python is the programming language you will use for both the Lambda function in the cloud and the query script on your Mac. VS Code is a free code editor where you will write your local Python script.
- Open your Terminal by pressing Cmd+Space to open Spotlight Search, typing Terminal, and pressing Enter.
- Check if Python 3.13 is already installed by running this command:
python3 --version
✔️ I see version 3.13
Python 3.13 is ready to go. Skip ahead to the VS Code install below.
ⓧ I see an older version
Your Mac ships with an older version of Python. You need 3.13 for this project because the Lambda function uses the python3.13 runtime.
- Open python.org/downloads/macos/ in your browser.
- Find the latest Python 3.13 release and click Download macOS installer.
- Double-click the downloaded .pkg file and follow the installer steps.
- When the install finishes, double-click Install Certificates.command in the /Applications/Python 3.13/ folder.
- Close and reopen Terminal, then run python3 --version again to confirm it now shows 3.13.
Still seeing the old version?
macOS may resolve python3 to the system Python even after installing 3.13. Try running python3.13 --version directly. If that works, use python3.13 and pip3.13 throughout this project instead.
ⓧ Command not found
Python is not installed yet. Let's get it from the official site.
- Open python.org/downloads/macos/ in your browser.
- Find the latest Python 3.13 release and click Download macOS installer.
- Double-click the downloaded .pkg file and follow the installer steps.
- The installer will ask for your Mac password to authorize the install. Enter it and continue.
- When the install finishes, double-click Install Certificates.command in the /Applications/Python 3.13/ folder.
- Close and reopen Terminal, then run python3 --version to confirm it shows 3.13.
Now install VS Code, the editor you will use to write your local query script.
- Open code.visualstudio.com in your browser.
- Click the download button for macOS. This downloads a .zip file.
- Double-click the .zip to extract it, then drag Visual Studio Code.app into your Applications folder.
- Open VS Code by pressing Cmd+Space, typing Visual Studio Code, and pressing Enter.
VS Code won't open?
macOS may block the app because it was downloaded from the internet. Right-click Visual Studio Code.app in Applications, choose Open, then click Open again on the confirmation dialog.
Ask for help: why won't VS Code open on my Mac?
Sign in to AWS and enable Bedrock access
Your feedback analyzer runs on AWS cloud services. Sign in to the AWS Console with your IAM user to get started.
- Open aws.amazon.com in your browser and sign in with your IAM user credentials.
- In the top-right corner of the console, click the region dropdown.
- Select US East (N. Virginia) us-east-1.
Why us-east-1?
US East (N. Virginia) has the broadest availability of Amazon Bedrock models, including the Claude model you will use. Setting your region now ensures all services you create are in the same place.
Next, enable access to Claude models through Amazon Bedrock.
- In the AWS Console search bar at the top, type Bedrock and select Amazon Bedrock.
- In the left sidebar, expand Bedrock configurations and click Model access.
- Click Modify model access.
- Find and select the Anthropic Claude models (check the boxes next to them).
- Click Next.
- Anthropic requires a one-time use-case form. Fill in a brief description (for example, "product feedback analysis for learning purposes"), accept the EULA, and click Submit.
- Wait a few minutes for the model access status to change to Access granted. Refresh the page to check.
What is Amazon Bedrock?
Amazon Bedrock is a managed service that gives you API access to foundation models (like Claude) without managing any infrastructure. You pay only for the tokens you send and receive.
Install the AWS CLI and verify your credentials
The AWS CLI lets you interact with AWS services from your Terminal. You will use it to upload files to S3 and to run your local Python script against DynamoDB.
- Install the AWS CLI by running this command in Terminal:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg" && sudo installer -pkg AWSCLIV2.pkg -target /
What does this command do?
The first part (curl) downloads the AWS CLI installer package from Amazon's servers. The second part (sudo installer) runs the macOS package installer to install it system-wide.
Your Terminal will ask for your Mac password because sudo requires administrator privileges. Type your password (it won't show characters) and press Enter.
- Verify the install succeeded by running:
aws --version
You should see a version number starting with aws-cli/2.
Command not found after installing?
Close your Terminal window and open a new one. The install adds aws to your path, but existing Terminal sessions may not pick it up until restarted.
Help me troubleshoot the AWS CLI install on macOS.
Your IAM user credentials should already be configured in the CLI. Let's verify the connection works.
- Verify the CLI is connected to your AWS account by running:
aws sts get-caller-identity
You should see a JSON response containing your account ID, user ARN, and user ID. This confirms your CLI can communicate with AWS.
Seeing an error about invalid credentials?
Double-check that you pasted the Access Key ID and Secret Access Key correctly. Run aws configure again to re-enter them. Make sure there are no extra spaces before or after the keys.
Help me fix my AWS CLI credentials.
Your environment is ready. Next up, you will create the S3 bucket and DynamoDB table that power your feedback pipeline.
Create the S3 Bucket and DynamoDB Table
Your environment is set up and ready to go. Now it's time to build the data infrastructure that your pipeline will run on.
Every piece of this pipeline needs a home. Raw feedback CSVs need a landing zone in the cloud. Structured analysis results need a database that supports fast queries by theme, type, or severity.
In this step, get ready to:
- Create an S3 bucket for storing raw feedback files.
- Set up a DynamoDB table for structured analysis results.
- Write a sample feedback CSV with 10 entries for testing.
Create the S3 bucket
Your pipeline starts when a CSV file lands in an S3 bucket. This bucket is the trigger point for everything that follows.
- In the AWS Console, click the search bar at the top of the page.
- Type S3 and select S3 from the results.
- Click Create bucket.
- For Bucket name, enter feedback-analyzer-raw- followed by your initials (for example, feedback-analyzer-raw-jd).
- Confirm the AWS Region is set to US East (N. Virginia) us-east-1.
- Leave all other settings at their defaults.
- Click Create bucket.
You should see your new bucket appear in the Buckets list.
Why do bucket names need your initials?
S3 bucket names must be globally unique across all AWS accounts worldwide. Adding your initials ensures your bucket name does not conflict with anyone else's.
🙋♀️ Seeing a 'Bucket already exists' error?
Try adding more characters to make the name unique (for example, feedback-analyzer-raw-jd123). As long as it starts with feedback-analyzer-raw- you are good to go.
help me fix a bucket name conflict in S3
Create the DynamoDB table
When your Lambda function analyzes feedback, it needs somewhere to store the structured results. DynamoDB is a NoSQL database that lets you query results by type, theme, or severity.
- Click the search bar at the top of the AWS Console.
- Type DynamoDB and select DynamoDB from the results.
- Click Create table.
- For Table name, enter FeedbackAnalysis.
- For Partition key, enter feedback_id and keep the type set to String.
- Under Table settings, select Customize settings.
- Under Read/write capacity settings, select Provisioned.
- Set Read capacity to 5 units. Turn off Auto scaling if it is enabled.
- Set Write capacity to 5 units. Turn off Auto scaling if it is enabled.
- Leave all other settings at their defaults.
- Click Create table.
Wait a few seconds for the table status to show Active. You can see this on the Tables list page or by clicking into the table details.
What is a partition key?
A partition key is the primary identifier for each item in a DynamoDB table. Every item must have a unique partition key value.
Your pipeline will generate a unique UUID for each feedback analysis result, so no two items will collide.
💡 Why provisioned capacity at 5 units?
DynamoDB offers two capacity modes: on-demand (pay per request) and provisioned (fixed throughput). Provisioned at 5 RCU and 5 WCU stays comfortably within the Always Free tier, which includes 25 RCU and 25 WCU per month.
🙋♀️ Table stuck in 'Creating' status?
DynamoDB tables typically become active within 10-20 seconds. If the status does not change, refresh the page. If it still shows creating after a minute, check that your region is set to us-east-1 in the top-right corner of the console.
help me troubleshoot my DynamoDB table not becoming active
Create a sample feedback CSV
Before wiring up the Lambda function, you need test data. This CSV file contains 10 realistic feedback entries mixing bugs, feature requests, praise, and complaints.
- In VS Code, open the integrated terminal by pressing Ctrl+` (the backtick key above Tab).
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir feedback-analyzer
cd feedback-analyzer
- Confirm the folder exists by running:
ls
You should see the feedback-analyzer folder listed in the output.
- Open this folder in VS Code by running:
code .
What does this command do?
The code . command opens the current folder as a workspace in VS Code. If VS Code asks you to trust the folder, click Yes, I trust the authors.
- In VS Code's Explorer sidebar, click the new file icon (or press Cmd+N on macOS / Ctrl+N on Windows).
- Save the file immediately as sample_feedback.csv by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Paste the following content into sample_feedback.csv:
id,feedback_text,source,date
1,"The app crashes every time I try to export a PDF. This has been happening for a week now.",support_ticket,2026-06-15
2,"Would love to see a dark mode option. My eyes get tired during late night sessions.",app_review,2026-06-18
3,"Your onboarding flow is fantastic! I was up and running in 5 minutes.",nps_survey,2026-06-20
4,"The search function returns irrelevant results. I searched for 'billing' and got articles about 'building'.",support_ticket,2026-06-21
5,"Please add integration with Slack. Our whole team lives there and switching context is painful.",feature_vote,2026-06-22
6,"Loading times have gotten worse since the last update. Dashboard takes 10+ seconds now.",app_review,2026-06-23
7,"Love the new collaboration features! Real-time editing is a game changer for our team.",nps_survey,2026-06-24
8,"Cannot upload files larger than 10MB. This is a dealbreaker for our design team.",support_ticket,2026-06-25
9,"Would be great to have keyboard shortcuts for common actions. Would speed up my workflow significantly.",feature_vote,2026-06-26
10,"The mobile app is nearly unusable. Buttons are too small and the navigation is confusing.",app_review,2026-06-27
What is in this test data?
The CSV contains 10 entries across four feedback sources: support_ticket, app_review, nps_survey, and feature_vote.
The entries intentionally mix different feedback types (bugs, feature requests, praise, complaints) so you can verify that Claude categorizes each one correctly.
- Save sample_feedback.csv by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Confirm the file exists in the Explorer sidebar on the left. You should see sample_feedback.csv listed under your feedback-analyzer folder.
Don't see the file in Explorer?
Make sure you opened the feedback-analyzer folder in VS Code (not a parent folder). Check the top of the Explorer sidebar to confirm it says FEEDBACK-ANALYZER.
If you saved the file to the wrong location, delete it and try again by pressing Cmd+S (macOS) or Ctrl+S (Windows) and navigating to Desktop/feedback-analyzer.
help me find my sample_feedback.csv file
✔️ Awesome, I've got everything!
Great. Double check you have saved sample_feedback.csv and you are ready to move on.
ⓧ I'd like to double check the full code
Here is the complete sample_feedback.csv file for comparison:
id,feedback_text,source,date
1,"The app crashes every time I try to export a PDF. This has been happening for a week now.",support_ticket,2026-06-15
2,"Would love to see a dark mode option. My eyes get tired during late night sessions.",app_review,2026-06-18
3,"Your onboarding flow is fantastic! I was up and running in 5 minutes.",nps_survey,2026-06-20
4,"The search function returns irrelevant results. I searched for 'billing' and got articles about 'building'.",support_ticket,2026-06-21
5,"Please add integration with Slack. Our whole team lives there and switching context is painful.",feature_vote,2026-06-22
6,"Loading times have gotten worse since the last update. Dashboard takes 10+ seconds now.",app_review,2026-06-23
7,"Love the new collaboration features! Real-time editing is a game changer for our team.",nps_survey,2026-06-24
8,"Cannot upload files larger than 10MB. This is a dealbreaker for our design team.",support_ticket,2026-06-25
9,"Would be great to have keyboard shortcuts for common actions. Would speed up my workflow significantly.",feature_vote,2026-06-26
10,"The mobile app is nearly unusable. Buttons are too small and the navigation is confusing.",app_review,2026-06-27
Your data infrastructure is in place. Next up, you will build the Lambda function that reads this CSV, sends each entry to Claude for analysis, and writes structured results to DynamoDB.
Build the Lambda Function
Your storage layer is ready. You have an S3 bucket to receive feedback files and a DynamoDB table to store structured results.
Now you need the compute layer that connects them. AWS Lambda lets you run code without managing servers. When a file lands in S3, Lambda runs your Python function automatically.
In this step, get ready to:
- Create a Lambda function with the Python 3.13 runtime.
- Grant the function permission to access S3, DynamoDB, and Bedrock.
- Write and deploy the feedback analysis code.
Create the Lambda function
You need a function that runs Python code in the cloud whenever a file is uploaded. Lambda handles the server infrastructure for you.
- In the AWS Console, type Lambda in the top search bar and select Lambda from the results.
- Click Create function.
- Select Author from scratch.
- For Function name, enter FeedbackAnalyzer.
- For Runtime, select Python 3.13.
- Leave Architecture set to x86_64.
- Under Permissions, keep the default Create a new role with basic Lambda permissions selected.
- Click Create function.
You should see a success banner and the function overview page for FeedbackAnalyzer.
The default timeout is 3 seconds, which is too short for AI model calls. Each Bedrock call can take several seconds, and you are processing 10 items sequentially.
- Click the Configuration tab.
- In the left sidebar, click General configuration.
- Click Edit.
- Set Timeout to 1 min 0 sec (60 seconds total).
- Click Save.
Why 60 seconds?
Lambda's maximum timeout is 900 seconds (15 minutes). You only need 60 seconds because your function processes 10 feedback items sequentially, and each Bedrock call takes a few seconds. This gives comfortable headroom without over-provisioning.
🙋♀️ Don't see Python 3.13 in the runtime list?
Make sure your region is set to US East (N. Virginia) in the top-right corner of the console. Some runtimes are not available in all regions.
help me find the right runtime for my Lambda function
Add permissions to the Lambda role
When Lambda created your function, it also created an IAM role with basic logging permissions. Your function also needs to read from S3, write to DynamoDB, and call Amazon Bedrock. You will attach three managed policies to grant these permissions.
- In the AWS Console, type IAM in the top search bar and select IAM from the results.
- In the left navigation pane, click Roles.
- Search for FeedbackAnalyzer and click the role name (it looks like FeedbackAnalyzer-role-xxxxx).
- Click Add permissions, then select Attach policies.
- Search for AmazonS3ReadOnlyAccess and check the box next to it.
- Clear the search, then search for AmazonDynamoDBFullAccess and check that box.
- Clear the search again, search for AmazonBedrockFullAccess, and check that box.
- Click Add permissions.
You should see all three policies listed on the role's Permissions tab alongside the existing basic execution policy.
What do these policies do?
- AmazonS3ReadOnlyAccess lets the function read the CSV file from your S3 bucket.
- AmazonDynamoDBFullAccess lets the function write structured results to your FeedbackAnalysis table.
- AmazonBedrockFullAccess lets the function call Claude via the Bedrock API for AI analysis.
🙋♀️ Can't find the role?
Lambda auto-generates the role name using the pattern FeedbackAnalyzer-role- followed by random characters. If you renamed your function or created multiple functions, search for the function name you used.
help me find the IAM role for my Lambda function
Write the Lambda code
Now you will write the Python code that reads feedback from S3, sends each item to Claude via the Bedrock Converse API, and stores structured results in DynamoDB.
- Return to your Lambda function by searching for Lambda in the console search bar, clicking Functions, then selecting FeedbackAnalyzer.
- Click the Code tab to open the inline editor.
- Select all the existing code in lambda_function.py and delete it.
- Copy and paste the following code into the editor:
import json
import csv
import boto3
import io
import uuid
s3_client = boto3.client("s3")
dynamodb = boto3.resource("dynamodb")
bedrock_client = boto3.client("bedrock-runtime", region_name="us-east-1")
TABLE_NAME = "FeedbackAnalysis"
MODEL_ID = "us.anthropic.claude-sonnet-4-6-20250514-v1:0"
def lambda_handler(event, context):
bucket = event["Records"][0]["s3"]["bucket"]["name"]
key = event["Records"][0]["s3"]["object"]["key"]
response = s3_client.get_object(Bucket=bucket, Key=key)
csv_content = response["Body"].read().decode("utf-8")
reader = csv.DictReader(io.StringIO(csv_content))
table = dynamodb.Table(TABLE_NAME)
results = []
for row in reader:
analysis = analyze_feedback(row["feedback_text"])
item = {
"feedback_id": str(uuid.uuid4()),
"original_id": row["id"],
"feedback_text": row["feedback_text"],
"source": row["source"],
"date": row["date"],
"theme": analysis.get("theme", "unknown"),
"sentiment": analysis.get("sentiment", "neutral"),
"severity": analysis.get("severity", "low"),
"type": analysis.get("type", "other"),
}
table.put_item(Item=item)
results.append(item)
return {
"statusCode": 200,
"body": json.dumps(f"Processed {len(results)} feedback items"),
}
def analyze_feedback(feedback_text):
system_prompt = """You are a product feedback analyst. Analyze the given feedback and return a JSON object with exactly these fields:
- theme: a short label for the topic (e.g., "performance", "ui_design", "export_feature", "onboarding", "search", "integrations", "file_upload", "mobile_experience", "collaboration", "keyboard_shortcuts")
- sentiment: one of "positive", "neutral", or "negative"
- severity: one of "low", "medium", "high", or "critical"
- type: one of "bug", "feature_request", "praise", or "complaint"
Return ONLY the JSON object, no other text."""
messages = [
{
"role": "user",
"content": [{"text": f"Analyze this product feedback:\n\n{feedback_text}"}],
}
]
response = bedrock_client.converse(
modelId=MODEL_ID,
messages=messages,
system=[{"text": system_prompt}],
inferenceConfig={"maxTokens": 256, "temperature": 0.1},
)
response_text = response["output"]["message"]["content"][0]["text"]
try:
analysis = json.loads(response_text)
except json.JSONDecodeError:
analysis = {
"theme": "unknown",
"sentiment": "neutral",
"severity": "low",
"type": "other",
}
return analysis
What does this code do?
This function has two main parts:
- lambda_handler is the entry point that Lambda calls. It extracts the bucket and file name from the S3 event, reads the CSV, loops through each row, calls analyze_feedback for AI categorization, and writes each result to DynamoDB.
- analyze_feedback sends a single feedback string to Claude via the Bedrock Converse API. The system prompt instructs Claude to return a JSON object with exactly four fields: theme, sentiment, severity, and type.
The MODEL_ID uses a cross-region inference profile (us.anthropic.claude-sonnet-4-6-20250514-v1:0) which routes requests across US regions for better availability.
💡 Why the Converse API instead of InvokeModel?
AWS recommends the Converse API as the consistent, model-agnostic interface for Bedrock. It handles model-specific payload formatting internally, so your code stays the same even if you swap Claude for a different model later.
- Click Deploy in the code editor toolbar.
You should see a green banner confirming the function was updated successfully.
Seeing a deployment error?
Make sure you deleted all the original default code before pasting. If you have duplicate function definitions or syntax errors from partial pastes, select all, delete, and paste the code again.
help me fix my Lambda deployment error
✔️ Awesome, I've got everything!
Great. Make sure you have clicked Deploy and saved your file.
ⓧ I'd like to double check the full code
Here is the complete lambda_function.py file. Make sure your code matches this exactly:
import json
import csv
import boto3
import io
import uuid
s3_client = boto3.client("s3")
dynamodb = boto3.resource("dynamodb")
bedrock_client = boto3.client("bedrock-runtime", region_name="us-east-1")
TABLE_NAME = "FeedbackAnalysis"
MODEL_ID = "us.anthropic.claude-sonnet-4-6-20250514-v1:0"
def lambda_handler(event, context):
bucket = event["Records"][0]["s3"]["bucket"]["name"]
key = event["Records"][0]["s3"]["object"]["key"]
response = s3_client.get_object(Bucket=bucket, Key=key)
csv_content = response["Body"].read().decode("utf-8")
reader = csv.DictReader(io.StringIO(csv_content))
table = dynamodb.Table(TABLE_NAME)
results = []
for row in reader:
analysis = analyze_feedback(row["feedback_text"])
item = {
"feedback_id": str(uuid.uuid4()),
"original_id": row["id"],
"feedback_text": row["feedback_text"],
"source": row["source"],
"date": row["date"],
"theme": analysis.get("theme", "unknown"),
"sentiment": analysis.get("sentiment", "neutral"),
"severity": analysis.get("severity", "low"),
"type": analysis.get("type", "other"),
}
table.put_item(Item=item)
results.append(item)
return {
"statusCode": 200,
"body": json.dumps(f"Processed {len(results)} feedback items"),
}
def analyze_feedback(feedback_text):
system_prompt = """You are a product feedback analyst. Analyze the given feedback and return a JSON object with exactly these fields:
- theme: a short label for the topic (e.g., "performance", "ui_design", "export_feature", "onboarding", "search", "integrations", "file_upload", "mobile_experience", "collaboration", "keyboard_shortcuts")
- sentiment: one of "positive", "neutral", or "negative"
- severity: one of "low", "medium", "high", or "critical"
- type: one of "bug", "feature_request", "praise", or "complaint"
Return ONLY the JSON object, no other text."""
messages = [
{
"role": "user",
"content": [{"text": f"Analyze this product feedback:\n\n{feedback_text}"}],
}
]
response = bedrock_client.converse(
modelId=MODEL_ID,
messages=messages,
system=[{"text": system_prompt}],
inferenceConfig={"maxTokens": 256, "temperature": 0.1},
)
response_text = response["output"]["message"]["content"][0]["text"]
try:
analysis = json.loads(response_text)
except json.JSONDecodeError:
analysis = {
"theme": "unknown",
"sentiment": "neutral",
"severity": "low",
"type": "other",
}
return analysis
Your Lambda function is deployed and has the permissions it needs. Next up, you will wire S3 to Lambda so uploading a CSV triggers the analysis automatically.
Connect S3 to Lambda and Test
Your Lambda function is deployed and ready to analyze feedback. But right now, it has no way to know when new feedback arrives in your S3 bucket.
In this step, you will wire S3 to Lambda using an event notification. When a CSV file lands in the bucket, S3 automatically triggers your function. You will then upload your sample feedback and watch the full pipeline process it into structured insights.
In this step, get ready to:
- Configure an S3 event notification to trigger your Lambda function on CSV uploads.
- Upload sample feedback and confirm the pipeline executes successfully.
- Review structured AI analysis results in DynamoDB.
Configure the S3 event trigger
An event-driven architecture means services react to things that happen rather than constantly polling for changes. Here, S3 will push a notification to Lambda the moment a new CSV arrives.
- Return to the Lambda console and select your FeedbackAnalyzer function.
- In the Function overview pane, click Add trigger.
- In the trigger source dropdown, select S3.
- Under Bucket, select your feedback bucket (feedback-analyzer-raw- followed by your initials).
- Under Event types, select PUT.
- Under Suffix, enter .csv so only CSV uploads trigger the function.
- Under Recursive invocation, select the acknowledgement checkbox.
- Click Add.
What does this trigger do?
The S3 event notification tells AWS to invoke your FeedbackAnalyzer function every time a file ending in .csv is uploaded (PUT) to your bucket.
The suffix filter prevents the function from firing on non-CSV files like images or JSON. Lambda automatically updates the function's resource-based policy to allow S3 to invoke it.
You should see the S3 trigger appear in the Function overview diagram on your Lambda page.
Seeing an error about destination configurations?
This happens if your bucket previously had a trigger configured for a Lambda function that no longer exists. Delete any old event notifications from your bucket's Properties tab under Event notifications, then try adding the trigger again.
Help me fix my S3 trigger configuration error.
Upload the sample CSV and verify
Now for the moment of truth. You will upload your sample feedback file and watch the entire pipeline execute automatically.
- Navigate to the S3 console and open your feedback-analyzer-raw- bucket.
- Click Upload.
- Click Add files and select your sample_feedback.csv file from your computer.
- Click Upload to confirm.
What happens next?
S3 detects the new .csv file and fires an event notification to your Lambda function. Lambda reads the CSV from S3, sends each of the 10 feedback items to Claude via Amazon Bedrock, and writes the structured results to DynamoDB. This takes about 30-60 seconds to complete.
Now confirm the function executed successfully by checking CloudWatch Logs.
- Navigate back to your FeedbackAnalyzer function in the Lambda console.
- Click the Monitor tab.
- Click View CloudWatch logs.
- Select the most recent Log stream (the one with the latest timestamp).
You should see log entries showing the function was invoked. Look for a return value containing Processed 10 feedback items in the log output.
Logs may take a moment to appear
CloudWatch Logs can take 5 to 10 minutes to show up after a function invocation. If you do not see a log stream yet, wait a minute and refresh the page.
🙋♀️ No log stream appearing?
Make sure you uploaded a file ending in .csv (not .CSV or .txt). The suffix filter is case-sensitive.
Also confirm your S3 trigger shows as Enabled in the Function overview diagram. If the trigger was not saved correctly, re-add it from the Lambda console.
Help me troubleshoot why my Lambda function is not being triggered by S3.
Review the results in DynamoDB
If your logs show a successful execution, your structured feedback is now stored in DynamoDB. Time to verify the AI actually categorized everything correctly.
- Navigate to DynamoDB in the AWS Console.
- Click Tables in the left sidebar.
- Select the FeedbackAnalysis table.
- Click Explore table items.
You should see 10 items in the table. Each item contains the original feedback text plus the AI-generated structured fields.
- Click on any item to expand it and verify it contains these fields:
- feedback_id (a UUID generated by the function).
- feedback_text (the original customer feedback).
- theme (e.g., performance, ui_design, export_feature).
- sentiment (positive, neutral, or negative).
- severity (low, medium, high, or critical).
- type (bug, feature_request, praise, or complaint).
- source and date (preserved from the original CSV).
Why structured extraction matters
Raw feedback like "the app crashes when I export" is useful to read, but impossible to query at scale. By extracting structured fields, you can now answer questions like "how many critical bugs do we have?" or "what is the most requested feature?" with a simple database query.
This is the core technique behind paid PM tools like Enterpret and Productboard Spark.
🙋♀️ Seeing 0 items in the table?
Check your CloudWatch logs for error messages. Common causes include the Lambda function timing out (if Bedrock takes too long) or a permission error on DynamoDB writes.
Confirm your Lambda role has AmazonDynamoDBFullAccess and AmazonBedrockFullAccess policies attached. Also verify that Bedrock model access for Anthropic Claude has been approved (this can take a few minutes after requesting).
Help me debug why my DynamoDB table is empty after Lambda executed.
Your serverless pipeline is fully operational. Raw customer feedback goes in, structured product insights come out. In the next step, you will build a local Python script to query these insights and display a prioritized report.
Query Insights Locally with Python
Your serverless pipeline is live. Raw feedback goes into S3, Lambda processes it through Claude, and structured results land in DynamoDB. The cloud-side work is done.
Now you need a way to actually read those insights. In this step, you will write a local Python script that connects to your DynamoDB table and displays the analyzed feedback as a prioritized report.
In this step, get ready to:
- Install boto3 locally so Python can talk to AWS services from your machine.
- Write a query script that scans DynamoDB and displays categorized feedback insights.
- Run the script and validate your end-to-end pipeline from raw CSV to prioritized product insights.
Install boto3 locally
Your Lambda function uses boto3 to interact with AWS services in the cloud. To query DynamoDB from your own machine, you need the same library installed locally.
- Install boto3 by running this command in your Terminal:
pip3 install boto3
What does this command do?
This installs the AWS SDK for Python. It gives your local scripts the same ability to call AWS services (S3, DynamoDB, Bedrock) that your Lambda function has in the cloud.
- Verify the install succeeded. You should see a message ending with Successfully installed boto3 (plus some dependency names).
Seeing a permission error?
If you see a permissions error, try running pip3 install boto3 --user instead. This installs the package into your user directory without needing admin access.
Help me troubleshoot my boto3 installation
Write the insights query script
This script connects to your FeedbackAnalysis table, pulls all the structured feedback items, and prints a formatted report showing exactly what a product manager needs to prioritize.
- In VS Code, create a new file called query_insights.py in the same folder as your sample_feedback.csv.
- Add the imports, DynamoDB connection, and scan function by pasting this code into query_insights.py:
import boto3
from collections import Counter
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
table = dynamodb.Table("FeedbackAnalysis")
def scan_all_items():
items = []
response = table.scan()
items.extend(response["Items"])
while "LastEvaluatedKey" in response:
response = table.scan(ExclusiveStartKey=response["LastEvaluatedKey"])
items.extend(response["Items"])
return items
What does this code do?
The top lines connect to your FeedbackAnalysis table in us-east-1 using the same AWS credentials you configured earlier with aws configure.
The scan_all_items() function handles DynamoDB pagination. DynamoDB returns results in pages. The while loop keeps fetching until there are no more pages left.
- Add the display function below scan_all_items() by pasting this code:
def display_insights(items):
print(f"\n{'='*60}")
print(f" PRODUCT FEEDBACK ANALYSIS REPORT")
print(f"{'='*60}")
print(f"\n Total feedback items analyzed: {len(items)}\n")
type_counts = Counter(item["type"] for item in items)
print(" Breakdown by Type:")
for feedback_type, count in type_counts.most_common():
print(f" {feedback_type}: {count}")
print(f"\n{'-'*60}")
print(" Severity Distribution:")
severity_counts = Counter(item["severity"] for item in items)
for severity in ["critical", "high", "medium", "low"]:
count = severity_counts.get(severity, 0)
print(f" {severity}: {count}")
print(f"\n{'-'*60}")
print(" Top Themes:")
theme_counts = Counter(item["theme"] for item in items)
for theme, count in theme_counts.most_common(5):
print(f" {theme}: {count} mentions")
print(f"\n{'-'*60}")
print(" Critical and High Severity Items:")
urgent = [item for item in items if item["severity"] in ("critical", "high")]
for item in urgent:
print(f" [{item['severity'].upper()}] ({item['type']}) {item['theme']}")
print(f" \"{item['feedback_text'][:80]}...\"")
print()
print(f"\n{'-'*60}")
print(" Feature Requests (sorted by severity):")
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
feature_requests = [item for item in items if item["type"] == "feature_request"]
feature_requests.sort(key=lambda x: severity_order.get(x["severity"], 4))
for item in feature_requests:
print(f" [{item['severity']}] {item['theme']}: \"{item['feedback_text'][:60]}...\"")
print(f"\n{'='*60}\n")
What does this function do?
This function takes the raw items from DynamoDB and produces a structured report with five sections:
- Total count of feedback items analyzed.
- Breakdown by type (bug, feature_request, praise, complaint).
- Severity distribution showing how many critical, high, medium, and low items exist.
- Top themes across all feedback, ranked by frequency.
- Feature requests sorted by severity so the most urgent ones appear first.
- Complete the script by adding the main block at the bottom of query_insights.py:
if __name__ == "__main__":
items = scan_all_items()
if not items:
print("No items found in DynamoDB. Upload a CSV to your S3 bucket first.")
else:
display_insights(items)
What does this block do?
The if __name__ == "__main__": guard ensures this code only runs when you execute the script directly (not when imported as a module). It calls scan_all_items() to fetch data, then passes the results to display_insights() for the formatted report.
- Save query_insights.py.
✔️ Awesome, I've got everything!
Great. Make sure you have saved query_insights.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete query_insights.py file. Make sure yours matches exactly:
import boto3
from collections import Counter
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
table = dynamodb.Table("FeedbackAnalysis")
def scan_all_items():
items = []
response = table.scan()
items.extend(response["Items"])
while "LastEvaluatedKey" in response:
response = table.scan(ExclusiveStartKey=response["LastEvaluatedKey"])
items.extend(response["Items"])
return items
def display_insights(items):
print(f"\n{'='*60}")
print(f" PRODUCT FEEDBACK ANALYSIS REPORT")
print(f"{'='*60}")
print(f"\n Total feedback items analyzed: {len(items)}\n")
type_counts = Counter(item["type"] for item in items)
print(" Breakdown by Type:")
for feedback_type, count in type_counts.most_common():
print(f" {feedback_type}: {count}")
print(f"\n{'-'*60}")
print(" Severity Distribution:")
severity_counts = Counter(item["severity"] for item in items)
for severity in ["critical", "high", "medium", "low"]:
count = severity_counts.get(severity, 0)
print(f" {severity}: {count}")
print(f"\n{'-'*60}")
print(" Top Themes:")
theme_counts = Counter(item["theme"] for item in items)
for theme, count in theme_counts.most_common(5):
print(f" {theme}: {count} mentions")
print(f"\n{'-'*60}")
print(" Critical and High Severity Items:")
urgent = [item for item in items if item["severity"] in ("critical", "high")]
for item in urgent:
print(f" [{item['severity'].upper()}] ({item['type']}) {item['theme']}")
print(f" \"{item['feedback_text'][:80]}...\"")
print()
print(f"\n{'-'*60}")
print(" Feature Requests (sorted by severity):")
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
feature_requests = [item for item in items if item["type"] == "feature_request"]
feature_requests.sort(key=lambda x: severity_order.get(x["severity"], 4))
for item in feature_requests:
print(f" [{item['severity']}] {item['theme']}: \"{item['feedback_text'][:60]}...\"")
print(f"\n{'='*60}\n")
if __name__ == "__main__":
items = scan_all_items()
if not items:
print("No items found in DynamoDB. Upload a CSV to your S3 bucket first.")
else:
display_insights(items)
Run the script and validate your pipeline
This is the moment of truth. Your script will reach across the internet to your DynamoDB table and pull back the structured insights that Claude generated from your raw feedback.
- Run the insights script by executing this command in your Terminal:
python3 query_insights.py
- Verify the output shows a formatted report with all five sections: total items analyzed, breakdown by type, severity distribution, top themes, and feature requests sorted by severity.
What should I see?
Your terminal should display a report titled PRODUCT FEEDBACK ANALYSIS REPORT with 10 total items analyzed. You will see counts for each type (bugs, feature requests, praise, complaints), a severity breakdown, the top themes across all feedback, and a prioritized list of feature requests.
The exact theme labels and severity assignments depend on how Claude categorized your feedback. Minor differences in theme naming are normal.
🙋♀️ Seeing "No items found in DynamoDB"?
This means the scan returned zero items. Check that your sample_feedback.csv was uploaded to S3 and that the Lambda function ran successfully. Return to CloudWatch Logs in the AWS Console to check for errors in your Lambda execution.
Also verify that your local AWS CLI is configured for the same region (us-east-1) where your DynamoDB table lives.
Help me figure out why my DynamoDB scan is returning empty results
You now have a complete end-to-end pipeline. Raw customer feedback goes into S3, gets analyzed by Claude via Lambda, and the structured insights are queryable from your local machine. Next up, tackle the Secret Mission to turn these insights into a weighted prioritization report.
Secret mission
Generate a Product Prioritization Report
Your analysis report shows individual feedback items, but a PM needs a prioritized list of themes ranked by demand. Extend your query script to aggregate feedback by theme, weight items by severity, and produce a Top 5 Priorities section ready for a roadmap meeting.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses serverless AWS resources that only incur costs when invoked, so idle resources cost nothing within the free tier.
Cost warning
Amazon Bedrock charges per token each time your Lambda function processes feedback. If you upload additional CSV files to your S3 bucket, the Lambda function triggers automatically and sends requests to Claude. Each invocation costs a fraction of a cent, but repeated uploads add up. Delete the S3 event trigger or the Lambda function if you want to prevent any future Bedrock charges.
Resources you used:
- S3 bucket: feedback-analyzer-raw-{initials} (stores uploaded CSV files).
- DynamoDB table: FeedbackAnalysis (stores structured feedback results).
- Lambda function: FeedbackAnalyzer (processes feedback through Claude).
- IAM role: FeedbackAnalyzer-role-xxxxx (permissions for the Lambda function).
- Local files: sample_feedback.csv, lambda_function.py, query_insights.py.
Keep everything running
No action needed. Choose this if you plan to upload more feedback CSVs or continue experimenting with the pipeline.
- Lambda, DynamoDB, and S3 are all within the Always Free tier for idle usage.
- You only pay for Bedrock when a new CSV triggers the Lambda function.
- Your local Python files remain on your machine.
- The AWS CLI and boto3 remain installed for future AWS projects.
Pause - I'll come back to this later
Shut down the trigger so nothing runs while you are away. Your data and code stay intact.
- In the AWS Console, navigate to your FeedbackAnalyzer Lambda function.
- Under **Configuration**, select **Triggers**.
- Select the S3 trigger and click **Disable**.
- This prevents new CSV uploads from invoking Bedrock. Re-enable the trigger when you return.
Delete - I don't want to use this again
Remove all project resources and start fresh. Run these commands in your terminal.
Delete the S3 bucket and all its contents:
aws s3 rb s3://[[BUCKET_NAME="feedback-analyzer-raw-your-initials"]] --force
Delete the DynamoDB table:
aws dynamodb delete-table --table-name FeedbackAnalysis
Delete the Lambda function:
aws lambda delete-function --function-name FeedbackAnalyzer
Delete the IAM role (you must detach policies first):
Finding your role name
Your Lambda role is named something like FeedbackAnalyzer-role-xxxxx where xxxxx is a random string. Find the exact name in the IAM Console under **Roles**, or check the Lambda function's **Configuration** > **Permissions** tab.
aws iam detach-role-policy --role-name [[ROLE_NAME="FeedbackAnalyzer-role-xxxxx"]] --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
aws iam detach-role-policy --role-name [[ROLE_NAME="FeedbackAnalyzer-role-xxxxx"]] --policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
aws iam detach-role-policy --role-name [[ROLE_NAME="FeedbackAnalyzer-role-xxxxx"]] --policy-arn arn:aws:iam::aws:policy/AmazonBedrockFullAccess
aws iam detach-role-policy --role-name [[ROLE_NAME="FeedbackAnalyzer-role-xxxxx"]] --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name [[ROLE_NAME="FeedbackAnalyzer-role-xxxxx"]]
Delete your local project files:
rm sample_feedback.csv lambda_function.py query_insights.py
Uninstall boto3 if you no longer need it:
pip3 uninstall boto3 -y
Nice Work!
Nice Work!
You just built a production-style serverless pipeline that turns raw customer feedback into structured, prioritized product insights. This is the same core architecture behind tools like Enterpret, Productboard Spark, and Gisti.
You've learned how to:
- Built a serverless event-driven pipeline using S3, Lambda, and DynamoDB that automatically processes data when a file is uploaded.
- Used Amazon Bedrock to invoke Claude for AI-powered structured extraction, turning unstructured text into categorized JSON with theme, sentiment, severity, and type.
- Created an IAM role with least-privilege policies connecting four AWS services, and wrote Python code using boto3 that interacts with AWS both in Lambda and locally.
- Secret Mission: built a weighted prioritization report that aggregates evidence by theme and ranks features by demand, ready for a roadmap meeting.
Ready to quiz yourself?