Part 7 of 7 · Email assistant series ~3 min read

Engineering reference: the email assistant architecture

Same system as the rest of the series, drawn purely for engineers. Service names, resource identifiers, region, Bedrock model IDs, and the actual flow operations — everything you’d need to recreate this in your own AWS account.

Posts 1–6 walk through the system in plain language. This page is the dense version — nothing softened, just the architecture as you’d sketch it on a whiteboard during a design review.

Full technical architecture: serverless email assistant in ap-southeast-1 A detailed engineering diagram of the entire email assistant. Three external surfaces at the top: GitHub (repo and Actions runner, OIDC token requestor); Google Drive (config folder containing knowledge file with FAQs, hours, tone, sender allowlist, with changes.watch push notifications); and the public internet email senders sending to SES inbound through MX records on a verified domain identity. Everything runs in a single AWS account in region ap-southeast-1 (Singapore). The AWS account contains five subsystems. Build and Deploy strip at the top: GitHub Actions exchanges with IAM OIDC Provider, assumes an IAM Role with a trust policy scoped to repo:owner/repo:ref:main, and runs SAM/CloudFormation to update the email-assistant-prod stack. Config Sync strip below: a Lambda Function URL named fn-config-sync receives Drive changes.watch notifications, validates the knowledge file, chunks and embeds it via Bedrock Titan Embeddings, and writes both the raw doc to S3 email-assistant-data/config/ and the vectors to an S3 Vectors index named vec-knowledge. Three runtime columns below. Reader (intake and parsing): SES Receipt Rule writes raw MIME to S3 email-assistant-data/raw/, an S3 ObjectCreated event invokes Lambda fn-reader (py3.14) which classifies the lane (auto-archive, AI-handle, or direct escalate) and on AI-handle parses the email into a clean envelope written to DynamoDB tbl-messages. Brain (per email): Lambda fn-brain runs on tbl-messages stream events, retrieves top-k passages from S3 Vectors, and invokes Bedrock global.anthropic.claude-haiku-4-5 with strict tool_use over four tools (answer, draft, escalate, archive); the choice + confidence score get written back to tbl-messages. Sender (carrying out the decision): Lambda fn-sender reads the decision from tbl-messages and routes — SES SendEmail with proper threading headers for answer; DynamoDB tbl-drafts plus an SNS push notification for draft; SES forward to the operator inbox for escalate; just an audit log row for archive. Cross-cutting bottom strip: DynamoDB tables tbl-messages, tbl-drafts, tbl-audit log every email and action; CloudWatch Logs are configured with RetentionInDays of 7 across every log group; SNS topics t-alarms and t-drafts notify on failures and pending drafts; AWS Budgets has a $10 monthly alarm; Lambda fn-archive runs on a separate weekly cron 0 3 SUN to move old raw email blobs to S3 Glacier Instant Retrieval storage class. GitHub github.com/owner/repo Actions runner · OIDC token requestor Google Drive config folder · knowledge.md, allowlist changes.watch push notifications Public email senders MX → SES inbound, verified domain inbound mail + outbound replies AWS Account Region: ap-southeast-1 (Singapore) · Bedrock via Global CRIS Build & Deploy IAM OIDC Provider token.actions.githubusercontent.com IAM Role trust: repo:owner/repo:ref:main SAM / CloudFormation stack: email-assistant-prod git push & request token AssumeRole sam deploy → creates stack resources below Config Sync Lambda Function URL fn-config-sync (chunks + embeds) Bedrock Titan Embeddings amazon.titan-embed-text-v2 S3 Vectors vec-knowledge (knowledge index) changes.watch notification Reader (intake & parsing) SES Receipt Rule verified domain · MX writes raw MIME S3 email-assistant-data/raw/ ObjectCreated AWS Lambda fn-reader (py3.14) parsed envelope DynamoDB tbl-messages (PAY_PER_REQUEST) → stream event triggers Brain inbound mail Brain (per email) DynamoDB Streams tbl-messages event AWS Lambda fn-brain (per envelope) retrieve S3 Vectors vec-knowledge (top-k chunks) InvokeModel Bedrock Haiku 4.5 global.anthropic.claude-haiku-4-5 strict tool_use: 4 tools → tool choice + score → tbl-messages Sender (act on the decision) AWS Lambda fn-sender (routes by tool) if answer SES SendEmail In-Reply-To, References headers if draft DynamoDB + SNS tbl-drafts · t-drafts ping if escalate SES Forward to operator inbox + brief if archive DynamoDB tbl-audit (no reply) → sender hears back, or doesn’t knowledge feeds fn-brain Cross-cutting DynamoDB tbl-messages, tbl-drafts, tbl-audit CloudWatch Logs RetentionInDays: 7 SNS t-alarms, t-drafts AWS Budgets budget-monthly: $10 Lambda fn-archive EventBridge cron(0 3 ? * SUN *) → moves old raw email blobs to S3 Glacier Instant Retrieval
Fig 7. Full architecture, ap-southeast-1. White boxes = AWS resources; dashed AWS container; dashed grey boxes = subsystem groupings; dashed grey arrows = config feed and side branches.

Read this top-down, then column-by-column

Top row is the three external surfaces. Below it, the AWS account contains five subsystems: Build & Deploy across the top, then Config Sync, then three runtime columns (Reader, Brain, Sender), with a Cross-cutting strip at the bottom. Inbound mail arrives at SES on the verified domain, lands in S3, fans out to fn-reader, which writes a parsed envelope to tbl-messages. The DynamoDB stream then triggers fn-brain, which retrieves passages from S3 Vectors and invokes Bedrock with strict tool_use over four tools. The choice and confidence score get written back to tbl-messages, which triggers fn-sender — routing to SES outbound, the draft queue, escalate-forward, or just an audit row, depending on which tool fired.

Naming conventions used in the diagram

  • Lambda functions: fn-<purpose>fn-reader, fn-brain, fn-sender, fn-config-sync, fn-archive.
  • DynamoDB tables: tbl-messages (every inbound, with parsed envelope and current state), tbl-drafts (pending human approvals), tbl-audit (every action ever taken).
  • SNS topics: t-alarms for general failures, t-drafts for pending-draft pushes to the operator’s phone.
  • S3 layout: single bucket email-assistant-data with prefixes raw/{date}/, config/, archive/.
  • S3 Vectors index: vec-knowledge — chunked + embedded knowledge file for top-k retrieval.

Region, model access, and email deliverability

Everything runs in ap-southeast-1 (Singapore) for low latency from the Philippines. Bedrock model invocations use the Global cross-Region inference profile (model IDs prefixed with global.) — data at rest stays in Singapore; inference may route to other regions for capacity. Pricing is the same as on-demand Singapore pricing.

Email deliverability is the part of email that catches out engineers who are used to webhooks. Three pieces have to be set up before any reply leaves your domain:

  • SES domain identity — verify the domain via DKIM CNAMEs in your DNS provider. Verified domains can both receive (with the right MX records) and send.
  • Custom MailFrom subdomainmail.yourdomain.com, with its own SPF/MX records, so SPF and DMARC alignment work correctly when SES sends on your behalf.
  • SES sandbox removal — new SES accounts are sandboxed (can only send to verified addresses). One short request form on the AWS console moves you to production limits.

The brain uses strict tool_use: four tool definitions (answer_directly, draft_for_review, escalate_to_human, archive_no_reply) with required parameter schemas including a confidence_score in [0, 1] and a citation_passage_id string. So the model can only emit a structured tool call — not a free-text reply. Free text would let it invent prices or promises; tool_use makes that mathematically impossible.

What’s deliberately not on the diagram

  • IAM policy details — per-Lambda execution role inline policies are minimal (one bucket prefix, one or two tables, SES on a single sending identity, Bedrock invoke on one model).
  • Per-business knowledge schema — the knowledge.md file is a single Drive doc with sections for FAQs, hours, services, policies, and tone. Updating sections updates the assistant’s answers without a deploy.
  • X-Ray tracing — on for fn-brain and fn-sender, sampling 100% during tuning, 10% in steady state. Mostly for confidence-score calibration, not latency.
  • The CloudFormation parameters for the Bedrock model ID and the embedding model are templated, so swapping models doesn’t require code changes.
  • Amazon WorkMail — AWS’s managed inbox product. If you want the operator’s “normal inbox” to also live in AWS, this slots in next to SES; for most teams, the operator inbox is just the existing Gmail or Outlook setup and no WorkMail is needed.
  • Bedrock Knowledge Bases — managed retrieval that can replace the explicit S3 Vectors + Titan Embeddings setup with a single connector. Bedrock Knowledge Bases now natively supports S3 Vectors as the storage engine, so the underlying primitives are the same; you just get an opinionated chunk-and-retrieve loop instead of running it yourself. Worth picking when the bring-your-own-knowledge-file pattern isn’t strictly required.
  • Bedrock Guardrails contextual grounding check — Bedrock’s managed grounding-and-relevance scoring layer. The custom confidence_score and citation_passage_id tool parameters in fn-brain are roughly the same idea hand-rolled; swapping to Guardrails moves the thresholds into console configuration and adds a second pair of eyes (PII redaction, prompt-attack detection, topic filters) on every model call. Worth turning on once the in-code thresholds are stable.
  • SES Mail Manager — the managed inbound rule engine. The April 2026 update added an Invoke Lambda function rule action plus optional mTLS on the ingress endpoint, making it a clean drop-in replacement for fn-reader’s lane-classification logic when the receipt-rule library grows beyond a handful of patterns. The traditional SES Receipt Rules path used in the diagram remains valid; Mail Manager is the upgrade, not a deprecation.

If you’re recreating this

Start with Build & Deploy alone (a single Lambda, no triggers). Once git push reliably updates an empty stack, verify a domain in SES and get a single “hello” auto-reply going on a hard-coded subject line. Then S3 inbound storage. Then fn-reader with just lane classification (no parsing yet). Then the parsed envelope. Then the Brain on a single hard-coded tool. Then the other three tools. Then the draft queue and the operator notifications. Cross-cutting (audit, logs, alarms, budget, archive) goes in from day one.

All posts