Running a Strands Agent on Lambda to Tag Product Reviews
Posted on October 26, 2025 • 7 minutes • 1489 words
Table of contents
What is Strands Agents
Strands Agents is an open source SDK from AWS for building AI agents with a model-first approach. You define a model, a system prompt, and optional tools. The agent loop handles planning and tool use.
It supports multiple providers such as Amazon Bedrock and integrates with the Model Context Protocol for tool discovery and composition. This lets you wire in external capabilities without changing your core agent logic.
Why Build This
I wanted a minimal but useful agent to test the SDK, as I had never used it before. For input it takes in a list of product reviews. The output is two lists of tags for Pros and Cons that can be shown at the top of a Product Detail Page or a reviews page. The realistic end-goal of this in production would be event driven; New review lands, fire a Lambda, summaries get written to a cache or database for the frontend.
This is a great fit for Lambda. The model runs in Bedrock so the function stays small and cheap. Cold starts are low and memory (and hence CPU) can stay low. The main latency is the call to the model itself, but with clever choice of model and by making it asyncronous then it wouldn’t be a problem.
Prototype at a Glance
- Frontend. Static HTML page posts
reviews[]to a Lambda Function URL for demo. - Lambda. Validates input, calls a Strands Agent, returns
{"pros":[],"cons":[]}. - Model. Claude 3 Haiku on Amazon Bedrock by default but this can be swapped by changing the model id only. Strands has native Bedrock support.
- Auth. Public for demo. In production this should use IAM, Cognito, or an API Gateway authoriser if HTTP driven.
- Future. Replace the Function URL with an event trigger on review ingestion and persist the result.
The Agent
The main logic for the agent lives in a file called agent_review.py. This is a very simple example of using Strands as its a single call into a Model.
from strands import Agent
SYSTEM = """You are an e-commerce review analyst. Input is a list of short customer reviews.
Analyze and generalize the reviews into HIGH-LEVEL themes. Rules:
1. CONSOLIDATE similar concepts: "excellent service", "very good service", "great service" → "reliable service"
2. AVOID specific details: dates, personal circumstances, technical specifics
3. FOCUS on actionable business insights, not individual complaints
4. GENERALIZE: "fast delivery", "quick delivery", "next day delivery" → "fast delivery"
5. EXCLUDE vague/unclear issues that don't represent clear themes
Examples of good themes:
- Pros: "fast delivery", "easy ordering process", "reliable service", "good value", "user-friendly website"
- Cons: "slow delivery", "poor customer service", "payment issues", "website problems", "reminder system issues"
Return ONLY minified JSON: {"pros":[],"cons":[]}. Maximum 8 items per list, focused on the most significant themes.
"""
def build_agent(model_id: str):
return Agent(system_prompt=SYSTEM, model=model_id)
def summarize_reviews(agent: Agent, reviews: list[str]) -> dict:
prompt = "REVIEWS:\n" + "\n".join(f"- {r}" for r in reviews) + "\nReturn JSON now."
result = agent(prompt)
# Defensive parse
import json, re
m = re.search(r"\{.*\}", str(result), re.S)
try:
data = json.loads(m.group(0)) if m else {}
except Exception:
data = {}
return {"pros": data.get("pros", []), "cons": data.get("cons", [])}
Defensive parse?
We all know that Agents can speak too much, even with a crafted prompt. The regex pull of the first JSON block keeps the output as deterministic as possible for the UI.
The Lambda
The handler itself just needs some basic boiler plate in the handler.py:
import json, os
from agent_review import build_agent, summarize_reviews
MODEL_PROVIDER = os.getenv("MODEL_PROVIDER", "bedrock")
MODEL_ID = os.getenv("MODEL_ID", "anthropic.claude-3-haiku-20240307-v1:0")
AGENT = build_agent(model_id=MODEL_ID)
def handler(event, context):
headers = {"content-type": "application/json"}
if event.get("requestContext", {}).get("http", {}).get("method") == "OPTIONS":
return {"statusCode": 200, "headers": headers, "body": ""}
try:
body = event.get("body") or "{}"
body = json.loads(body) if isinstance(body, str) else body
reviews = body.get("reviews", [])
if not reviews or not isinstance(reviews, list):
return {"statusCode": 400, "headers": headers, "body": json.dumps({"error":"reviews[] required"})}
out = summarize_reviews(AGENT, reviews)
return {"statusCode": 200, "headers": headers, "body": json.dumps(out)}
except Exception as e:
return {"statusCode": 500, "headers": headers, "body": json.dumps({"error": str(e)})}
As is best practice, this creates the agent once, outside the handler to ensure it’s kept cached in between invocations.
Infrastructure-as-Code
For this example I used CDK
. The main stack.py can be seen here:
from constructs import Construct
import aws_cdk as cdk
from aws_cdk import (
Duration,
aws_lambda as _lambda,
aws_lambda_python_alpha as lambda_python,
aws_iam as iam,
)
class ReviewSummAgentStack(cdk.Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
fn = lambda_python.PythonFunction(
self, "AgentFn",
entry="lambda",
index="handler.py",
handler="handler",
runtime=_lambda.Runtime.PYTHON_3_13,
architecture=_lambda.Architecture.ARM_64,
memory_size=256,
timeout=Duration.seconds(30),
environment={
"MODEL_PROVIDER": "bedrock",
"MODEL_ID": "anthropic.claude-3-haiku-20240307-v1:0"
}
)
# Bedrock invoke permissions. Scope to model ARNs in real usage.
fn.add_to_role_policy(iam.PolicyStatement(
actions=[
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
"bedrock:GetFoundationModel",
"bedrock:ListFoundationModels"
],
resources=["*"]
))
# Allow Marketplace backed models where needed
fn.add_to_role_policy(iam.PolicyStatement(
actions=[
"aws-marketplace:ViewSubscriptions",
"aws-marketplace:Subscribe",
"aws-marketplace:Unsubscribe"
],
resources=["*"]
))
# Function URL for demo
url = fn.add_function_url(
auth_type=_lambda.FunctionUrlAuthType.NONE,
cors=_lambda.FunctionUrlCorsOptions(
allowed_origins=["*"],
allowed_headers=["*"],
allowed_methods=[_lambda.HttpMethod.ALL],
),
)
cdk.CfnOutput(self, "FunctionUrl", value=url.url)
I chose 256 MB for this demo, which in itself is actually probably overkill. After all, its not running the LLM in the Lambda function itself. The function only parses JSON and calls Bedrock. The heavy lifting is all remote - if you do add RAG calls, raise the timeout first, not the memory.
Frontend
For the frontend I put together a simple single HTML file that posts lines to the Function URL and renders tags for Pros and Cons. This is just a simple API call and a form.

What I Observed
Simplicity. This whole example was very quick and simple to put together. Using a SDK to do a bunch of the boilerplate really does help.
Clustering quality. With short e-commerce reviews, Claude 3 Haiku gave me consistent clusters of generalised topics from the reviews. It fits the constraints in the system prompt. If you need richer themes, then likely you would need to move to a larger model for the same prompt. Bedrock model choice is a one line change in this setup though.
Latency. Cold starts are negligible on ARM with low memory. Most of the invocation time is the model call, but Strands supports streaming if you want progressive rendering.
Cost. Cost scales with tokens. The agent is simple and calls the model once. As long as you keep inputs short it’s low cost, so maybe don’t give it 100’s of raw reviews! Add a cap for the number of reviews per request.
Failure modes and guard rails
Over-specific themes. If the prompt invites specifics, the model will mirror them. The rule set in the system prompt therefore pushes it to high level tags. Keep examples tight, and iterating on the prompt with many examples is key.
JSON drift. Agents can talk, they in fact seem to love to talk. The regex extract helps with this issue. Even better would have been to wrap the agent with a schema validator and retry with a corrective prompt.
Prompt injection. User reviews can contain anything, the documentation has some guidance that is worth a read. Always consider Bedrock Guardrails before going to production.
Throughput. Function URLs are fine for a demo but use API Gateway with throttling and WAF for public endpoints. For high volume, consider an SQS or EventBridge trigger and process in batches.
Making it Event Driven
The natural next step is to remove the Function URL and wire the Lambda to review events. A rough plan I have in my head:
- Reviews land in your ingest service.
- Emit an EventBridge event.
- A small rule triggers the agent Lambda with a payload of new reviews or a review id.
- Lambda runs the agent, writes
pros[]/cons[]into a cache or table. - Frontend reads the pre-computed tags, not the model.
This removes UI-to-model coupling and gives you retries and DLQs.
Why Strands Instead of a Plain Bedrock SDK Call
You could call Bedrock directly. Strands earns its keep once you need:
Provider portability. Switch between Bedrock, Anthropic, OpenAI, or local providers through a single abstraction. Useful in multi-region or mixed estates.
Tools. If you decide to enrich reviews with a product catalogue lookup, drop in a tool. If you want to connect to external systems through MCP, the SDK already knows how.
Operations and Security
- Scope the Bedrock permissions to the model ARN and your region.
- Remove the public Function URL. Put an authoriser in front or just trigger from events.
- Add structured logs. Store the input hash, model id, token counts, and latency for cost control.
- Pin your dependencies. Strands is under active developement. Read the release notes and test before you bump versions.
Closing thoughts
This is the smallest useful agent I could build with Strands. The SDK keeps the code short and simple. Lambda stays light. The model choice lives in configuration only. For a real system I would run this off events, persist the output, add dead-letter-queues, and strong observability. For my next experiment, I’d like to add RAG and some MCP calls, see what I can get it to do!
