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.
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.
| 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) |
- 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.
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.
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.
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.
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.
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.
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.
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.
For the moving parts of each user-visible workflow, see the per-flow pages in about/:
- Auth & AG-UI session bootstrap
- Segment creation
- Customer profile (list + detail)
- Segment EDA
- Resource CRUD (campaign / template / customer)
- Immediate campaign send
- Scheduled campaign send
- Campaign engagement analytics
- KB-backed resource search
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, theDockerfile(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-dataThe 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.
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.pyThe simpler path is to install GnuWin Make
or run from WSL — the existing make targets work unchanged on either.
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)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.
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)
| 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.
The solution uses two complementary configuration mechanisms:
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.
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=trueKnowledge Base two-pass deploy: On fresh AWS accounts, the first deploy with
enableKnowledgeBase=truemay 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 runmake deploy(orcdk 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.
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.
This project is licensed under the MIT License — see LICENSE for details.

