Part 7 of 7 · Daily briefing bot series ~3 min read

Engineering reference: the briefing bot 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 — no softening, just the architecture as you’d sketch it on a whiteboard during a design review.

Full technical architecture: serverless daily briefing bot in ap-southeast-1 A detailed engineering diagram of the entire daily briefing bot. Three external surfaces at the top: GitHub (repo and Actions runner, OIDC token requestor), Google Drive (shared folder containing two files — sources and topic — with changes.watch push notifications), and the operator’s inbox (email destination for the digest). 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 for the github.com identity provider, assumes an IAM Role with a trust policy scoped to repo:owner/repo:ref:main, and runs SAM/CloudFormation to update the briefing-bot-prod stack. Config Sync strip below: a Lambda Function URL named fn-config-sync receives Drive changes.watch notifications, validates the new content, and writes to S3 briefing-bot-data/config/ (sources.json and topic.md). Three runtime columns below. Ingestor (scheduled fan-out): EventBridge Scheduler fires on cron 0 22 daily (06:00 SGT), invokes Lambda fn-dispatcher (python3.13 on arm64) which reads sources from S3 and sends one message per source to one of three SQS queues (q-rss, q-api, q-page, each with its own dead-letter queue), where Lambda workers (fn-rss-worker, fn-api-worker, fn-page-worker) consume them and write items to S3 briefing-bot-data/raw/{date}/ with deduplication via DynamoDB tbl-fingerprints. Ranker (3-stage filter): EventBridge Scheduler fires on cron 15 22 daily (06:15 SGT, 15 minutes after the ingestor), invokes Lambda fn-ranker, which reads raw items, applies cheap rule filters, computes embeddings via Bedrock amazon.titan-embed-text-v2:0 for similarity scoring, and for borderline items only invokes Bedrock global.anthropic.claude-haiku-4-5 in JSON mode for a final keep/skip decision. Keepers are written to S3 briefing-bot-data/keepers/{date}.json. Postman (event-driven): the S3 PutObject event on the keepers prefix triggers Lambda fn-postman, which composes the digest email (headline, one-line summary, why-it-matters, link per item) and sends via Amazon SES with configuration set “digest” from the verified domain. The send is recorded to DynamoDB tbl-digest-archive, and SES bounce notifications publish to SNS t-digest-failures. Cross-cutting bottom strip: DynamoDB tbl-audit logs every action, CloudWatch Logs are configured with RetentionInDays of 7 across every log group, SNS topic t-alarms emails the operator on failures, AWS Budgets has a $5 monthly alarm, and Lambda fn-watch-renewer runs on a separate weekly cron 0 3 SUN to renew the Drive watch channel before its 7-day expiry. GitHub github.com/owner/repo Actions runner · OIDC token requestor Google Drive folder · sources, topic changes.watch push notifications Operator inbox email destination delivered via Amazon SES 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: briefing-bot-prod git push & request token Actions runner AssumeRole sam deploy → creates stack resources below Config Sync Lambda Function URL fn-config-sync (validates, writes) S3 briefing-bot-data/config/ (sources, topic) changes.watch notification read by all 3 runtimes below Ingestor (scheduled fan-out) EventBridge Scheduler cron(0 22 * * ? *) · 06:00 SGT AWS Lambda fn-dispatcher (py3.13, arm64) SendMessage SQS q-rss, q-api, q-page (+ dlqs) AWS Lambda × 3 fn-rss-worker · fn-api-worker fn-page-worker PutObject · PutItem S3 + DynamoDB raw/{date}/ + tbl-fingerprints → Ranker reads at 06:15 SGT Ranker (3-stage filter) EventBridge Scheduler cron(15 22 * * ? *) · 06:15 SGT AWS Lambda fn-ranker (py3.13, arm64) InvokeModel Bedrock Titan Embed v2 amazon.titan-embed-text-v2:0 if borderline Bedrock Haiku 4.5 global.anthropic.claude-haiku-4-5 JSON mode: keep/skip/reason PutObject S3 keepers/{date}.json → PutObject triggers Postman Postman (event-driven) S3 PutObject event on keepers/{date}.json AWS Lambda fn-postman (composes digest) SendEmail Amazon SES configuration set: digest from: bot@your-domain PutItem DynamoDB tbl-digest-archive SNS t-digest-failures (bounces) → digest email at 07:00 SGT config flows to all three runtimes Cross-cutting DynamoDB tbl-audit (every action) CloudWatch Logs RetentionInDays: 7 SNS t-alarms (email) AWS Budgets budget-monthly: $5 Lambda fn-watch-renewer EventBridge cron(0 3 ? * SUN *) → renews Drive watch channel before 7-day expiry
Fig 7. Full architecture, ap-southeast-1. White boxes = AWS resources; dashed AWS container; dashed grey boxes = subsystem groupings; dashed grey arrows = config feed to runtimes.

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 (Ingestor, Ranker, Postman), with a Cross-cutting strip at the bottom. The dashed grey arrows from the Config Sync output to each runtime column show the only cross-subsystem data dependency — all three runtime Lambdas read the latest config from S3 on every invocation.

Naming conventions used in the diagram

  • Lambda functions: fn-<purpose> — e.g. fn-dispatcher, fn-ranker, fn-postman.
  • DynamoDB tables: tbl-<name> — e.g. tbl-fingerprints, tbl-digest-archive, tbl-audit.
  • SQS queues: q-<name> with paired q-<name>-dlq. One queue per source type.
  • SNS topics: t-<name>t-alarms for general failures, t-digest-failures for SES bounces.
  • S3 layout: single bucket briefing-bot-data with prefixes config/, raw/{date}/, keepers/{date}.json, archive/.

Region and Bedrock model access

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.

Crons are written in UTC (cron(0 22 * * ? *) = 06:00 SGT next day) so the digest lands before the operator’s morning coffee.

What’s deliberately not on the diagram

  • IAM policy details — per-Lambda execution role inline policies are minimal (one bucket prefix, one table, one queue as appropriate).
  • Per-channel delivery alternates — if you use Telegram or Slack instead of SES, the Postman column substitutes a Lambda Function URL call to the platform’s API and a corresponding secret in Secrets Manager.
  • X-Ray tracing — on for the Ranker Lambda only, sampling 10% — useful when tuning the similarity threshold.
  • The CloudFormation parameter for Bedrock model ID is templated, so swapping models doesn’t require code changes.

If you’re recreating this

Start with Build & Deploy alone (a single Lambda, no triggers). Once git push reliably updates an empty stack, add Config Sync next so you have a place to put your sources and topic. Then the Ingestor, then the Ranker, then the Postman — each on a real source, end to end, before adding the next. Cross-cutting (audit, logs, alarms, budget, watch renewer) goes in from day one.

All posts