The worker core · cutover done

The worker is the task_consumer Lambda

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).

What it is

Python Lambda

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.

What it does

Generate & gate

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.

Guarantee

Shadow / never sends

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.

Generation pipeline

Cron tick to drafted task, on AWS

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.

hop 1 Producer

POST /generate

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.

hop 2 Bus

SQS tasks-ingest + DLQ

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.

hop 3 Consumer

task_consumer → Bedrock

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.

store DynamoDB

…-tasks single-table

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.

Path: n8n cron → producer Lambda → SQS tasks-ingest → task_consumer (Bedrock) → DynamoDB pending_review. The ECS /api/maestro/generate endpoint stays an ack-only stub (desired_count=0) for manual / backfill use — not the hot path (ADR 0001, Path B).
Gabrielle — components by layer (DDD + ACL)
Animated · interactive — the worker's components arranged by layer (domain · application · adapters), with the anti-corruption layer isolating Airtable, HubSpot and Bedrock behind the drafting core. Open full ↗
Drafting in the rep voice

What the consumer reads per message

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.

Playbook + kill-switch

Workflows & Automations SSOT

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).

Identity + guardrails

Agent Settings + Guardrails-General

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.

Rep voice + inbox

Employee DB by owner

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.

Contacts

HubSpot list / Airtable view

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.

The Governor gate

ALLOW · HOLD · DEFER · BLOCK

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).

DecisionWhenEffect
ALLOWNo suppression, no cap breach, highest-priority family for the contact.The task is written normally for review.
DEFERRolling 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.
BLOCKSuppression list / blocked domain / explicit opt-out.Never written — the contact is off-limits.
HOLDReserved for rep overrides.In the enum for forward-compat; Phase 1 emits DEFER instead.
Threshold gating: the consumer self-scores each draft 0–100. confidence ≥ threshold (the Governor confidenceThreshold, default 60, authoritative for all playbooks — the per-playbook SSOT threshold is deprecated for gating) → pending_review with a draft (Standard queue); below → needs_review with a blank draft for manual triage. A log_call spec emits a call task instead of an email.
After the worker

Review in the Hub, then send

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.

Review queue

Hub v2 tasks-v2

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.

Send

message-dispatcher Lambda

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.

Cleanup pending

Retire the dual worker

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).

Status · what remains

Cutover done, email live to clients

Live: generation is fully on AWS — the consumer drafts into DynamoDB, the Hub reads it, and on Approve & Send the message-dispatcher emails the real client (Governor sendDevelopmentMode=false as last verified 2026-08-07; flip it to true to redirect to internal test inboxes). The pending step is retiring the legacy n8n send worker so the dispatcher is the sole sender (avoid double-send) — owner Farid, sign-off Jess. The VPS itself has already been retired (#152).