Skip to content

aws-samples/sample-agentic-campaign-manager

Repository files navigation

Agentic Campaign Manager

Agentic Campaign Manager demo

Disclaimer: AWS code samples are example code that demonstrates practical implementations of AWS services for specific use cases and scenarios. These application solutions are not supported products in their own right, but educational examples to help our customers use our products for their applications. As our customer, any applications you integrate these examples into should be thoroughly tested, secured, and optimized according to your business's security standards & policies before deploying to production or handling production workloads.

What this is

A multi-agent email marketing platform that runs the full campaign lifecycle — customer segmentation, template authoring, scheduling, sending, and engagement analytics — through natural-language conversation. An orchestrator routes user intent to five specialist agents, all hosted on Amazon Bedrock AgentCore Runtime and reachable from a React SPA over an authenticated, streaming AG-UI session. AG-UI lets agents render interactive UI inline in the chat — confirmation forms for destructive actions, segment-discovery wizards, Vega-Lite analytics charts, resource pickers — so the conversation is as much a workspace as it is a transcript.

Tech stack

Layer Service
LLM provider Amazon Bedrock (Anthropic Claude)
Agent hosting Amazon Bedrock AgentCore Runtime
Agentic framework Strands Agents SDK
Agent–UI protocol AG-UI (CopilotKit React client over SSE)
Knowledge base Amazon Bedrock Knowledge Bases on Amazon OpenSearch Serverless
Guardrails Amazon Bedrock Guardrails
Metadata store Amazon DynamoDB
Customer data lake Apache Iceberg on Amazon S3, queried via Amazon Athena
Email sending Amazon SES v2
Engagement event capture Amazon Kinesis Data Firehose → S3 → AWS Glue + Athena
Out-of-band workers AWS Lambda (campaign executor, sync, refresh, KB ingest)
Auth Amazon Cognito (Hosted UI + PKCE)
Edge Amazon CloudFront + AWS WAF
Infrastructure AWS CDK (TypeScript)

What it does

  • Customer profile management — chat-driven find/create/update/delete on the customer table (Iceberg via Athena); changes mirror to a DynamoDB cache within ~10 seconds for hot-path personalization.
  • Customer segmentation — natural-language audience definitions become Athena SQL; previewed before save, persisted as membership rows in DynamoDB, and refreshed hourly so campaigns hit current data.
  • Segment EDA — clicking "Analyze segment" runs an analytics agent that emits Vega-Lite visual breakdowns (plan, city, age, cohort) into the segment's Analytics tab.
  • AI-generated email templates — agents author HTML from a prompt and validate {{variables}} against the customer schema (case- sensitive) so {{First_Name}} vs {{first_name}} is caught before send time.
  • Campaign scheduling and sending — immediate sends invoke a Lambda executor directly; scheduled and recurring sends fire from EventBridge rules. Both paths hydrate the recipient list from DynamoDB and send through SES with engagement tracking.
  • Engagement analytics — ask in plain English ("why did deliveries dip last week?"); the agent queries the SES event lake via Athena and renders Vega-Lite charts inline.
  • KB-backed resource search — campaigns, segments, and templates are indexed in a Bedrock Knowledge Base for semantic lookup ("find the segment I made yesterday").
  • Human-in-the-loop & guardrails — destructive operations pause for inline confirmation; Bedrock Guardrails screen every model call; AWS WAF protects the edge.

Architecture

Architecture diagram

Frontend

The SPA is a React + TypeScript app built with Vite and the CopilotKit AG-UI client. It's served as a static bundle from an S3 bucket fronted by CloudFront with origin access control, and AWS WAF (managed common ruleset, IP reputation, and a 2000-request/5-minute rate limit) sits at the edge. Sign-in goes through Amazon Cognito's Hosted UI using PKCE; the resulting JWT is attached to every request.

Once authenticated, the browser opens a streaming session to AgentCore Runtime over Server-Sent Events. AG-UI lets agents push interactive UI directly into the chat — confirmation forms before destructive actions, segment-discovery wizards, Vega-Lite charts, resource pickers — so the conversation doubles as the workspace. Side-panel reads (campaign / segment / template / customer lists) hit a small REST API instead of round-tripping through the agent, keeping the dashboard responsive while the chat does the heavy thinking.

AgentCore Runtime, Strands Agents SDK, Guardrails

All six agents (one orchestrator plus five specialists) run inside a single container image stored in Amazon ECR and hosted by Amazon Bedrock AgentCore Runtime. CDK builds the image from agentcore-source/ as a LINUX_ARM64 asset, pushes it to the bootstrap ECR repository, and points the AgentCore Runtime resource at it — there is no ECS, EKS, Fargate, or Lambda in between. The browser reaches AgentCore through a second CloudFront distribution (separate from the SPA's, no caching, SSE-friendly timeout) that forwards Authorization and session-id verbatim. AgentCore exposes the container behind /invocations (SSE) and /ping, validates the Cognito JWT at the boundary, and proxies AG-UI traffic to the FastAPI app on port 8080.

Inside the container, the Strands Agents SDK implements the Agents-as-Tools pattern: the orchestrator's tool list contains five *_expert wrappers, each of which streams a sub-agent. Every agent shares one model factory that attaches an Amazon Bedrock Guardrail (content filters + prompt-attack detection) to the Anthropic Claude model on Bedrock, so guardrail evaluation happens on every LLM call without per-agent wiring. The container also opens read paths to a Bedrock Knowledge Base on OpenSearch Serverless for semantic resource search, and writes turn-scoped state slices (nav_command, customer_results, chart payloads) back through the AG-UI stream so the SPA can take over UI mid-tool-call.

Campaign specialist

The campaign specialist owns the campaign record itself — name, sender, segment reference, template reference, scheduling configuration, and the soft-delete lifecycle. It does not schedule, execute, or report on sends; those are owned by other specialists, and the system prompt enforces the boundary so the agent hands off cleanly. Per turn it reads and writes the campaigns table in DynamoDB (status-indexed via a GSI), cross-checks the referenced segment row for readiness, and resolves template-ID ULIDs against the template metadata table.

Sender validation goes through Amazon SES v2 — both ListEmailIdentities and GetEmailIdentity, with explicit domain-level catch-all support so a verified domain authorizes any address under it. The agent never writes to SES; for templates it only calls GetEmailTemplate to confirm existence on update. Free-text search ("any loyalty-points campaign") routes through Amazon Bedrock Knowledge Bases — a hybrid (semantic + BM25) retrieve against an index that is kept fresh by a side-pipeline: DynamoDB Streams on the campaigns table feed a kb-sync Lambda that renders each row to S3, an SQS-debounced kb-ingest-debouncer Lambda batches up to a minute of changes into a single Bedrock StartIngestionJob, and the resulting vector index lives in OpenSearch Serverless.

Segmentation specialist

The segmentation specialist runs two surfaces in one agent — customer CRUD against the source-of-truth Apache Iceberg table and the named-segment lifecycle. Customer reads, inserts, updates, and deletes are executed as Athena SQL against the Iceberg table in the customer-analytics Glue database; the agent calls get_customer_schema_tool against AWS Glue Data Catalog first so generated SQL matches actual columns. When an Iceberg write commits, an S3 event triggers the iceberg-sync Lambda, which refreshes the corresponding row in the DynamoDB customer cache within 5–10 seconds — that cache is what the campaign executor reads at send time.

Segment creation is a two-phase preview-then-commit handshake. Phase one validates the user's SQL and runs a short Athena preview (sample rows + total count) so the SPA can render a confirmation dialog before any DynamoDB write. On confirm, the agent writes a DRAFT row, runs SELECT customer_id FROM (<user sql>) through Athena, streams the customer IDs back inline via the GetQueryResults paginator — no parquet, no S3 round-trip — and snapshot-replaces the rows in the customer-segment-membership table (GSI-queried for the old set, BatchWriteItem-deleted, then BatchWriteItem-put with the new set). The segments row flips to READY with the member count, and a last_membership_change timestamp invalidates the segment EDA cache. An hourly EventBridge rule and a per-segment refresh Lambda repeat the same shape of query off-band so segments stay current. Free-text segment search uses the same shared Bedrock KB pipeline as campaigns and templates.

Email template specialist

The email template specialist keeps metadata in DynamoDB and the renderable body in Amazon SES — the two are written in lock-step, with a drift flag on the metadata row so a partial failure is reconcilable on the next list. Each template is identified by a ULID template_id; legacy templates created directly in SES are adopted-on-read into DynamoDB the first time they're listed, so the agent has one canonical view regardless of where the template originated. Creates, updates, and soft-deletes hit SES v2 (CreateEmailTemplate / UpdateEmailTemplate / DeleteEmailTemplate / GetEmailTemplate) and the metadata table in parallel.

Variable validation extracts {{placeholders}} from subject, HTML, and text and checks them against the customer schema, catching case mismatches separately from genuinely missing fields so the agent can suggest a fix. Free-text template search ("welcome email with a discount") goes through the same shared Bedrock KB pipeline as campaigns and segments, with one extra step: the kb-sync Lambda also calls SES GetEmailTemplate to fold the stripped HTML body into the indexed text, so semantic search hits content like "Smart AI" inside the body, not just metadata fields.

Campaign scheduling specialist

The scheduling specialist owns when a campaign sends and the data-layer confirmation gate that authorises it, but never sends mail itself — it hands off to a separate campaign-executor Lambda. The agent reads requires_confirmation / confirmation_status off the campaigns table in DynamoDB; on first call without a confirmed flag it returns a validation payload (sender verified, segment ready, template valid, recipient count) so the orchestrator can render confirm_campaign_launch in the SPA. Calling the specialist twice with tweaked args cannot bypass the flag.

For an immediate send, after confirmation the agent calls lambda:Invoke directly on the executor function. For a scheduled or recurring send, it calls events:PutRule with a cron ScheduleExpression and events:PutTargets to attach the executor Lambda as a target with a prebaked JSON input (campaign_id, action, schedule metadata) — note this is classic EventBridge Rules, not the newer EventBridge Scheduler. Both trigger paths end in the same executor Lambda, which paginates the customer-segment-membership GSI, BatchGetItems the latest attributes from the DynamoDB customer cache, calls SES v2 SendBulkEmail with the configured SES configuration set, and writes a per-message row to the email_delivery table. Engagement events flow back through that same configuration set into the analytics pipeline.

Analytics specialist

The analytics specialist is read-only and has two distinct surfaces backed by Amazon Athena. The engagement surface queries pre-aggregated views (campaign_analytics, campaign_summary, daily_summary, bounce_analysis) plus a raw ses_events_raw partitioned table in the SES analytics Glue database. Those tables are kept fresh by a streaming pipeline outside the agent: every SES event flows through an SES event destination into Amazon Kinesis Data Firehose, which converts JSON to Parquet, partitions by year/month/day/hour, and writes to S3; a partition-management Lambda registers new partitions in Glue Data Catalog so Athena can read them without waiting on a crawler.

The segment EDA surface runs the same SQL machinery against the customer-analytics Glue database. When the user clicks "Analyze segment" in the SPA, the orchestrator routes to this specialist with the segment row in context; the agent generates up to four breakdowns (plan composition, top cities, age histogram, registration cohort), runs each via Athena, and streams rows back to the orchestrator inline. The orchestrator turns the rows into render_chart calls tagged with source_kind='segment_eda' so the SPA renders them on the segment's Analytics tab, and writes the prose summary to a small DynamoDB cache (the SPA writes the bulk of the cache directly via REST, since it already has the chart payloads from the live stream). Set-overlap questions across multiple segments bypass Athena entirely — they're answered by GSI Queries against the customer-segment-membership table.

Per-flow detail

For the moving parts of each user-visible workflow, see the per-flow pages in about/:

Quick start

Prerequisites: Node.js 18+, Python 3.9+, AWS CLI configured, AWS CDK CLI, Docker (or Finch/Colima), and enable access to Anthropic Claude Sonnet 4 in the Amazon Bedrock console. Install Python dependencies for demo-data scripts: pip install -r customer-data/requirements.txt.

Supported Regions: This solution is pinned to us-east-1. The region is hardcoded in src/app.ts, the Dockerfile (AWS_DEFAULT_REGION), and the AgentCore agent config ARN. Deploying to other regions requires updating all three locations.

# 1. Deploy the backend (AgentCore + storage + auth + edge + WAF + SPA bucket)
make deploy

# 2. Build and publish the React SPA to the CloudFront-fronted bucket
make deploy-frontend

# 3. Create a Cognito user to sign in with
EMAIL=you@example.com PASSWORD='Pass12!@#' make seed-user

# 4. (Demo data) Generate sample customers
make seed-data

# 5. (Demo data) Load into Iceberg + DynamoDB cache
make upload-data

The SPA URL is printed by make deploy-frontend. Sign in with the credentials from step 3.

Steps 4–5 are optional — the app deploys empty without them, but the segmentation and analytics agents have nothing to query until demo data is loaded.

SES Sandbox Mode: New AWS accounts start with SES in sandbox mode. In sandbox, emails can only be sent to verified email addresses. To send campaigns to arbitrary recipients, request production access in the SES console or verify each recipient address individually.

Windows (no make)

The Makefile is a thin wrapper around the underlying commands. On Windows, run them directly in PowerShell:

# 1. Deploy
npx cdk deploy EmailMarketingInfra --require-approval never -c enableKnowledgeBase=true

# 2. Build + publish the SPA (run setup-env.sh under Git Bash / WSL, or
#    set the env vars manually from the stack outputs)
cd frontend; bash setup-env.sh; npm install; npm run build; cd ..
$STACK = "EmailMarketingInfra"
$BUCKET = aws cloudformation describe-stacks --stack-name $STACK --query "Stacks[0].Outputs[?ExportName=='$STACK-SpaBucketName'].OutputValue" --output text
$DIST   = aws cloudformation describe-stacks --stack-name $STACK --query "Stacks[0].Outputs[?ExportName=='$STACK-SpaDistributionId'].OutputValue" --output text
aws s3 sync frontend/dist s3://$BUCKET --delete
aws cloudfront create-invalidation --distribution-id $DIST --paths "/*"

# 3. Create a Cognito user
$POOL = aws cloudformation describe-stacks --stack-name $STACK --query "Stacks[0].Outputs[?ExportName=='$STACK-UserPoolId'].OutputValue" --output text
aws cognito-idp admin-create-user --user-pool-id $POOL --username you@example.com --user-attributes Name=email,Value=you@example.com Name=email_verified,Value=true --message-action SUPPRESS
aws cognito-idp admin-set-user-password --user-pool-id $POOL --username you@example.com --password "Pass12!@#" --permanent

# 4-5. Demo data
python customer-data/seed_customer_data.py
python customer-data/init_iceberg_table.py
python customer-data/load_demo_data_iceberg.py

The simpler path is to install GnuWin Make or run from WSL — the existing make targets work unchanged on either.

Local development

make setup-frontend   # auto-configures frontend/.env from CDK outputs + npm install
make run-frontend     # Vite dev server (default port 5173, falls back to 5174+ if busy)

Stack inventory

All 16 CDK constructs live under src/constructs/ and are wired in src/email-marketing-stack.ts.

Construct What it provisions
StorageConstruct DynamoDB tables: campaigns, segments, assets, customers, email-delivery, template metadata, segment membership, segment EDA cache, campaign analytics cache
AnalyticsConstruct S3 raw-events + Athena results buckets, Glue DB, Athena workgroup, Firehose stream + SES event destination
SegmentationConstruct Customer-data S3 bucket + Glue DB + Athena workgroup for Iceberg-backed customers
AuthConstruct Cognito User Pool + Hosted UI + PKCE SPA client
AgentCoreConstruct Bedrock AgentCore Runtime, container image built from agentcore-source/ as a CDK Docker asset (LINUX_ARM64) and pushed to the bootstrap ECR repo, IAM role, JWT authorizer wired to Cognito
CampaignExecutorConstruct Lambda that reads the segment-membership index in DynamoDB + customer cache and sends via SES
SegmentRefreshConstruct Hourly EventBridge rule + scheduler Lambda + per-segment refresh Lambda that re-runs each segment's SQL and diffs membership
CustomerSyncConstruct DynamoDB Streams → Lambda → S3 Parquet export of customer changes
IcebergSyncConstruct Bi-directional sync between Iceberg and the DynamoDB customer cache
ResourcesRestConstruct REST API for side-panel reads (campaigns / segments / templates / customers)
KnowledgeBaseConstruct (optional, -c enableKnowledgeBase=true) Bedrock KB + OpenSearch Serverless vector store
KbSyncConstruct (optional) DDB Streams → S3 → SQS → debounced KB StartIngestionJob
GuardrailsConstruct Bedrock Guardrail (content filters + prompt-attack), attached per-model
WafConstruct CLOUDFRONT-scoped WAF: managed common ruleset + IP reputation + 2000/5min rate limit
EdgeApiConstruct CloudFront distribution in front of AgentCore (Authorization + session-id forwarded, no caching, 120s SSE timeout)
SpaHostingConstruct S3 bucket + CloudFront distribution (OAC) for the React SPA build

Knowledge Base is gated by the enableKnowledgeBase CDK context flag — default off because OpenSearch Serverless has a non-trivial idle floor. make deploy enables it; cdk deploy without the flag does not.

Cost note: Enabling the Knowledge Base provisions OpenSearch Serverless with a minimum of 4 OCUs (2 indexing + 2 search). At $0.24/OCU-hour × 730 hours/month this adds ~$700/month to your bill even when idle.

Repo layout

agentic-campaign-manager/
├── about/              # Architecture diagram + per-flow pages (00-08)
├── agentcore-source/   # AgentCore Runtime container source — orchestrator + specialists + skills
├── customer-data/      # Sample-data generation + Iceberg bootstrap scripts
├── frontend/           # React + TypeScript SPA (Vite, CopilotKit AG-UI client)
├── lambda/             # Out-of-runtime Lambdas: campaign-executor, customer-sync,
│                       # iceberg-to-dynamodb-sync, iceberg-maintenance,
│                       # segment-refresh, segment-refresh-scheduler,
│                       # kb-sync, kb-ingest-debouncer, analytics, resolvers
├── src/                # CDK app
│   ├── email-marketing-stack.ts
│   └── constructs/     # 16 modular constructs (see Stack inventory)
└── Makefile            # Build + deploy targets (see Common operations)

Common operations

Target What it does
make deploy cdk deploy EmailMarketingInfra -c enableKnowledgeBase=true
make deploy-frontend Build SPA + sync to S3 + CloudFront invalidation
make seed-user Create / reset a Cognito user (EMAIL=… PASSWORD=…)
make seed-data Generate customer-data/sample_customers.csv
make upload-data Load CSV into Iceberg via Athena + warm DynamoDB cache
make backfill-templates Repopulate EmailTemplateMetadataTable from existing SES templates
make backfill-kb Seed Bedrock KB from existing DDB rows (only needed when enabling KB on a non-empty stack)
make setup-frontend Pull stack outputs into frontend/.env, npm install
make run-frontend Start the Vite dev server on port 5173
make build-frontend Build frontend/dist against current stack outputs
make clean Remove cdk.out, __pycache__, .pytest_cache

make help lists the same set with one-line descriptions read from the Makefile.

Configuration

The solution uses two complementary configuration mechanisms:

cdk_config.json

Static configuration values consumed by CDK at synth time. Key settings:

Key Purpose Default
sesConfigurationSetName Name of the SES Configuration Set used for engagement-event streaming to S3 via Firehose (none — must be set for analytics)

The Analytics agent depends on engagement events streaming to S3. Set sesConfigurationSetName in cdk_config.json before deploying. The CDK stack will auto-create the configuration set if it doesn't already exist.

CDK context flags

Runtime flags passed via -c on the command line or in cdk.json:

Flag Purpose Default
enableKnowledgeBase Provision the Bedrock Knowledge Base + OpenSearch Serverless vector store false

Example:

npx cdk deploy EmailMarketingInfra -c enableKnowledgeBase=true

Knowledge Base two-pass deploy: On fresh AWS accounts, the first deploy with enableKnowledgeBase=true may fail because OpenSearch Serverless reports its collection as ready before the data plane is fully consistent. If you encounter a "Bad Authorization" or "no such index" error from Bedrock, simply run make deploy (or cdk deploy) a second time — the collection will be stable by then. This two-pass requirement only applies to the initial deployment and will be eliminated once an automated wait mechanism is added.

Without the flag, all OSS/KB resources are skipped and the solution runs without semantic search capabilities.

Compliance Considerations

Important: This sample application processes personal data including email addresses, names, demographic attributes, and engagement metrics. If you deploy this system:

  • GDPR Compliance: When handling EU resident data, you must implement appropriate data protection measures including consent management, data subject rights (access, deletion, portability), and privacy notices. See the AWS GDPR Center for guidance.
  • Data Retention: Configure lifecycle policies and deletion workflows appropriate to your jurisdiction and use case.
  • Consent Management: Implement opt-in/opt-out mechanisms and honor unsubscribe requests through the SES suppression list.

This is a sample implementation for educational purposes. You are responsible for ensuring your deployment meets all applicable regulatory requirements.

License

This project is licensed under the MIT License — see LICENSE for details.

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors