Software Engineering, Architecture and AWS Serverless Technology from makit
Running a Strands Agent on Lambda to Tag Product Reviews
October 26, 2025

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

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.

Agent Frontend

What I Observed

Failure modes and guard rails

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:

  1. Reviews land in your ingest service.
  2. Emit an EventBridge event.
  3. A small rule triggers the agent Lambda with a payload of new reviews or a review id.
  4. Lambda runs the agent, writes pros[]/cons[] into a cache or table.
  5. 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:

Operations and Security

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!

Follow me

If you are interested in Coding, AWS, Serverless or ML