The sales draft engine no longer lives on the legacy ClawBot VPS. It is an AWS-native serverless pipeline: n8n crons POST to a producer, SQS buffers, and the task_consumer Lambda drafts each email with Bedrock in the rep’s voice, runs the Governor gate, and writes one task to DynamoDB — pending_review, never sent. Same bot (Gabrielle = ClawBot = Sales_Assistant), new engine (Maestro).
Python 3.12 Lambda hv-hub-v2-development-task-consumer, SQS-triggered (batch 1, ReportBatchItemFailures), pure boto3 + stdlib. Drafts via Bedrock Claude, writes to DynamoDB. us-east-2. $0 idle, auto-scales.
Per message: resolve the SSOT playbook + kill-switch, load Gabrielle’s prompt + guardrails, pull contacts, resolve the rep’s voice, draft with a 0–100 confidence self-score, pass the Governor, write the HITL task.
The consumer writes pending_review / needs_review only. It is structurally incapable of sending — a separate sender (the Unified Sender: the message-dispatcher Lambda) handles outbound, only after a human approves in the Hub.
A cron tick becomes a reviewable draft through three serverless hops — no warm process to fall over, a durable buffer with a DLQ, and resume-where-left-off on any crash.
The 11 active n8n generator crons (10 “Auto Tasks —” + the Gabrielle DFU-01 cron, re-pointed 2026-06-08) POST to the producer Lambda behind API Gateway. It returns a sub-second 202 ack and enqueues the body to SQS — thin, so n8n never times out on a big list.
Durable buffer (visibility 120s, maxReceiveCount 5). A message is deleted only after the consumer finishes OK — a crash makes it reappear, so zero loss. Bad messages DLQ alone and raise a CloudWatch alarm.
SQS-triggered Lambda. Resolves config, drafts with Bedrock Claude (us.anthropic.claude-sonnet-4-6), passes the Governor, and writes the task. ReportBatchItemFailures isolates a single bad message.
Conditional PutItem keyed on a per-contact dedup id (c_<sha1(email)>) — writes only when no open task exists for that contact, so re-ticks don’t pile up. Streams feed the audit / learning loop.
The Lambda replicates the legacy ClawBot generation contract from live config (all in Airtable base appUDQ65M1lSnSM5p), then drafts each email in the assigned rep’s voice. Every read degrades to a safe default, so a partial config never fails the batch.
Resolved directly by playbook_id: Status, Confidence Threshold, Suggested Action Spec, Bundling Rule, the linked Template. Status is the only kill-switch — a stop value (Deprecated / Paused / Blocked…) skips the playbook (#268).
Gabrielle’s system prompt, voice and never-do (record recK11xdc82ArNEX2) plus the active Guardrails-General rules — the 150-word cap (tighter wins over her 250), pricing hard-block, suppression check.
The contact’s HubSpot owner → the rep’s voice samples + from_inbox (parsed from the signature), so the draft sounds like the assigned rep. No owner match → jessica@ fallback.
Pulls from a HubSpot v3 ILS list (membership → batch-read) or an Airtable Orders view, optionally enriched with deals. Suppression_List fail-closed: if it can’t load, drafting halts rather than draft ungated.
Before a drafted touch is written, GOV-01 (the Contact Governor) decides whether it goes out now. The gate lives in the consumer path, reads a per-contact touch log, and enforces frequency caps and family precedence (GOVERNOR_ENABLED=true in dev, fail-closed by default).
| Decision | When | Effect |
|---|---|---|
| ALLOW | No suppression, no cap breach, highest-priority family for the contact. | The task is written normally for review. |
| DEFER | Rolling cap breach (1/14d, 2/30d per contact; 1/week per account) or a lower-priority family collides with an open higher one. | Held with a defer_until; the resurfacer re-runs it through check when the date arrives. |
| BLOCK | Suppression list / blocked domain / explicit opt-out. | Never written — the contact is off-limits. |
| HOLD | Reserved for rep overrides. | In the enum for forward-compat; Phase 1 emits DEFER instead. |
The consumer’s job ends at pending_review. A human reviews each task in the Hub v2 tasks-v2 queue with six universal actions, and only an approval triggers an outbound send — through a separate sender, never the consumer.
Six reviewer actions: Approve & Send, Edit (manual or Edit-with-AI, an in-place Bedrock rewrite), Reject, Snooze, Comment, Escalate. All source-agnostic in QueueActionsService.
The Unified Sender: SQS-triggered, idempotent (OUTBOX#), routes the approved message by channel (email Gmail / Slack / WhatsApp) — email is live to real clients today (Governor sendDevelopmentMode=false). The Hub’s /approved enforces the send-throttle first (per-rep daily cap + 14-day cooldown). The legacy maestro_sender sales sender (Gmail poll) ships inert beside it — SEND_DRY_RUN=true + EventBridge rule born DISABLED — so it delivers nothing.
A legacy n8n “AWS Send Worker (Maestro) — DRY-RUN” still polls the same approved queue. Email is already live to real clients (the message-dispatcher sends real; Governor sendDevelopmentMode=false) while the older maestro_sender Lambda is inert. The n8n worker is in dry-run and today just writes a misleading failed status — but now that email reaches clients it must be retired to avoid a double send. The VPS itself has already been retired (#152).