Follow a single outreach email from idea to inbox on the live pipeline. Gabrielle (the sales draft bot) generates the draft through Maestro — the AWS engine that replaced the old ClawBot VPS — landing it in DynamoDB as pending_review. A rep reviews it in the Hub v2 queue, then Approve & Send enqueues it onto SQS for the message-dispatcher Lambda (the Unified Sender). That send reaches the real client today: the send gates are live Governor switches in /admin/governor (no deploy), and as last verified 2026-08-07 globalSendEnabled=true and sendDevelopmentMode=false — email goes to the real recipient. (Flipping sendDevelopmentMode=true would redirect every email to the developmentTestEmails inboxes, but it is currently OFF.)
Gabrielle (= ClawBot = Sales_Assistant) is the sales draft bot. Its config — system prompt, voice, guardrails, kill-switch — stays in Airtable; only the engine moved. It drafts, it never sends.
The cutover is done: generation moved off the VPS/n8n onto AWS. n8n crons POST /generate → SQS → the task_consumer Lambda drafts via Bedrock into DynamoDB …-tasks (us-east-2).
Every draft is pending_review until a rep acts in the Hub v2 queue: Approve & Send, Edit (incl. Edit-with-AI), Reject, Snooze, Comment, or Escalate. Nothing reaches a customer without a human.
An n8n "Auto Tasks" cron ticks for a playbook (e.g. Account Growth follow-up) and POSTs /generate to the producer Lambda — a thin sub-second ACK that enqueues onto SQS …-tasks-ingest. The task_consumer Lambda pulls the message, loads Gabrielle's prompt + voice + guardrails, pulls the contacts (HubSpot list or Airtable Orders view), drafts with Bedrock Claude, and writes one row to DynamoDB as pending_review. It never sends.
The thin producer ACKs in under a second so the n8n cron never times out (it gives up at ~60s). A message is deleted only after the consumer succeeds — a crash mid-draft just replays after the visibility timeout, so zero loss, with a DLQ for poison messages.
Only the task store moved to DynamoDB. Gabrielle's config — Agent Settings (system prompt, voice, signature), the Workflows & Automations SSOT (cadence, thresholds), guardrails, suppression and templates — stays in Airtable, including the Status kill-switch the consumer respects.
The draft surfaces in the Hub v2 /queue — a NestJS API + Next.js 15 app on ECS Fargate at hubv2.apphappenventures.com. The rep sees the draft, its confidence, the sources it was grounded on, and the deal/contact links, then takes one of six actions. The Contact Governor (GOV-01) has already gated the task — ALLOW / DEFER / BLOCK / DROP against frequency caps and a per-contact touch log — before it ever reaches the queue (only ALLOW gets drafted; the rest are skipped).
Approve & Send, Edit (incl. Edit-with-AI — an in-place Bedrock rewrite the rep reviews before sending), Reject, Snooze, Comment, Escalate. By design there is no bulk-approve: these are real customer conversations. Reject and Escalate accept an optional reason (free text or a structured reason code); Comment appends to the thread without auto-advancing the status.
Edits are captured as edited_content alongside the original draft, and every action writes before/after to Audit_Log. That diff is the learning signal for improving future drafts — nothing is discarded.
On Approve & Send the task flips to resolved and the API enqueues a typed message onto SQS …-outbound-messages. The message-dispatcher Lambda — SQS-triggered, not a poll — claims each message idempotently (OUTBOX# in DynamoDB; claim refusal = already sent → skip, no double-send), routes by channel, and for email builds the RFC822 message and sends through the Gmail API impersonating the rep's inbox via domain-wide delegation. It sends for real: as last verified 2026-08-07 the Governor has sendDevelopmentMode=false, so email reaches the real client. The sandbox exists but is OFF — setting sendDevelopmentMode=true in /admin/governor would redirect every message to developmentTestEmails instead. (A separate SEND_DRY_RUN env kill-switch on the dispatcher, currently false, can force log-only.)
The message-dispatcher is the unified Sender: it owns the cross-cutting concerns once — send as the right rep (per-rep Gmail via service account + domain-wide delegation), claim-before-send, dry-run safety, and uniform audit. Its email adapter was ported verbatim from the old maestro_sender; real Slack and WhatsApp adapters already ride behind the same dispatcher (SMS is still a stub).
A legacy n8n "AWS Send Worker (Maestro)" still polls the /approved queue alongside the inert maestro_sender Lambda (both in dry-run, EventBridge disabled) — while the live sender is the message-dispatcher. Today the n8n worker just leaves a misleading failed status, but now that email is live to real clients it must be retired: an approved task it also delivered would reach the client twice.
Per ADR 0001 the store is DynamoDB (one single-table, not Postgres) and ingestion is Path B (SQS producer/consumer). Generation and send are two independent serverless paths that meet only at the DynamoDB task and the Hub's /queue read/action API — no shared god-table, no chatty calls.
Trigger: n8n "Auto Tasks" cron → POST /generate (API Gateway)
Producer: …-task-producer Lambda (Node 22) · 202 ACK <1s → SQS …-tasks-ingest + DLQ (maxReceive 5)
Consumer: …-task-consumer Lambda (Python 3.12) · SQS event-source · drafts with Bedrock claude-sonnet-4-6
Store: DynamoDB hv-hub-v2-development-tasks · single-table (PK/SK + GSI1 + Streams) · idempotent PutItem on task_guid · status pending_review
Brain config: Airtable (Agent Settings, SSOT, guardrails, templates) — read, not moved
Review UI/API: Hub v2 — NestJS API + Next.js 15 on ECS Fargate (hubv2.apphappenventures.com) reading DynamoDB via GET /api/queue
Gate: Contact Governor (GOV-01) in the consumer · ALLOW / DEFER / BLOCK (HOLD reserved, not yet emitted) · touch log + frequency caps + Status kill-switch
Sender: message-dispatcher Lambda · SQS …-outbound-messages event-source → idempotent claim → channel adapter (email Gmail DWD / Slack / WhatsApp)
Send mode: SEND_DRY_RUN=false and the Governor sendDevelopmentMode=false (as last verified 2026-08-07) → email reaches the real client; flip sendDevelopmentMode=true to redirect to developmentTestEmails · idempotent OUTBOX# claim-before-send · DLQ for poison messages · status → sent/failed + Audit_Log
Credentials: Secrets Manager · per-rep Gmail SA + HubSpot/Airtable tokens, never in code
DynamoDB's schemaless single-table fits the still-evolving HITL schema; PutItem ConditionExpression gives idempotency; Streams feed the audit/learning loop; idle cost is ~$0. The durable SQS bus + DLQ mean a consumer crash replays instead of losing work. And generation and send stay decoupled — the consumer only ever writes pending_review, so nothing reaches a customer until a human approves and a single sender dispatches it.
The "Auto Tasks" generation crons were re-pointed off the VPS to the producer /generate (verified by ID). The consumer drafts live into DynamoDB; the legacy backfill rows were imported at parity.
The Hub v2 sales queue reads + acts on DynamoDB; Approve & Send is un-gated (two-click confirm with sign-off) and a real end-to-end send was verified. The Contact Governor gate is live in dev.
Email and Slack are already live to real recipients (Governor globalSendEnabled=true, sendDevelopmentMode=false, sendSlackEnabled=true; WhatsApp still off via sendWhatsappEnabled=false). What's left is retiring the legacy n8n "AWS Send Worker (Maestro)" (the inert maestro_sender Lambda can go with it) so the message-dispatcher is the sole sender — otherwise an approved task both pick up double-sends to the client. Turning on WhatsApp is a later per-channel flag.
Generation has been 100% on AWS (DynamoDB-verified) since the 2026-06 cutover — no live generator hits the VPS. The leftover (disabled) VPS POST nodes were dropped, the historical task_queue_sales table left as an archive, the ClawBot VPS (187.77.29.73) shut down, and the leaked Basic credential rotated (#152). The Airtable robin_tasks table is still being retired (DRV/MCP writers pending re-point — #259).
| Old VPS / n8n concern | AWS primitive (live) | Why |
|---|---|---|
| Generation cron entrypoint | API Gateway → producer Lambda | Sub-second 202 ACK so the n8n cron never times out. |
| Ingestion buffer | SQS …-tasks-ingest + DLQ | Durable, resume-where-left-off, poison-message isolation. |
| Drafting brain (VPS FastAPI) | task_consumer Lambda + Bedrock | Packaged code, version-controlled, $0 idle, auto-scales. |
| Draft model | Bedrock claude-sonnet-4-6 | In-account inference profile, us-east-2. |
| task_queue_sales store | DynamoDB single-table | Schemaless HITL fit, idempotent PutItem, Streams for audit. |
| Approval UI (Airtable Interfaces) | Hub v2 (NestJS + Next.js, ECS) | Reads/acts on DynamoDB via /api/queue. |
| Frequency / suppression rules | Contact Governor (GOV-01) | Central ALLOW/DEFER/BLOCK/DROP gate + touch log + caps. |
| Claim / no double-send | claim-before-send (status) | Refusal = another sender took it → skip. |
| 5-credential Gmail switch | Secrets Manager + Gmail DWD | Send as the rep via service account, IAM-scoped, no key in code. |
| Send trigger | SQS outbound-messages → message-dispatcher | Channel-agnostic dispatch, idempotent claim, dry-run-safe. |
| Execution history | CloudWatch + #hv-alerts | Structured logs; SNS → Slack relay + Sentinel poller. |
| Replay / audit / learning | DynamoDB Streams (→ S3/Athena later) | Edit-diffs + Audit_Log feed the learning loop. |
Only the task store moved to AWS. Gabrielle's brain — the SSOT kill-switch, Agent Settings, guardrails, suppression and templates — stays in Airtable per ADR 0001. The robin (R3 ops) and finance (Frank) task queues migrated to the same DynamoDB table (source distinguishes the bot); see Robin and Current Flow.