Back to Proactive Customer Communication

Developer Handover Pack

Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.

4 pagesPDF · Technical
FS-DOC-04Technical

Developer Handover Pack

Proactive Customer Communication

[YourCompany.com] · Customer Support Department · Prepared by FullSpec · [Today's Date]

This document is the primary technical reference for the FullSpec team building the Proactive Customer Communication automation. It covers the current-state process map, all agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. Everything required to scaffold, configure, and hand over the automation is contained here. The process owner's responsibilities are limited to confirming the status ruleset, sensitivity thresholds, and channel preference data quality before build begins.

01Process snapshot

Step
Name
Description
1 BOTTLENECK
Check CRM for Status Changes
Support Agent opens HubSpot each morning and scans manually for records that have changed to a notification-eligible status since the last check. Relies entirely on the agent remembering to look. Time cost: 20 min/cycle.
2
Cross-Reference Customer Contact Preferences
Agent checks whether the customer prefers email, SMS, or in-app messaging by reviewing their HubSpot profile or a shared Google Sheet. Preferences are frequently out of date or missing. Time cost: 10 min/cycle.
3 BOTTLENECK
Draft the Customer Update Message
Agent writes a status-appropriate message from scratch, referencing the customer name, relevant details, and next steps. Message quality and tone varies between team members. Time cost: 15 min/cycle.
4 BOTTLENECK
Get Approval for Non-Standard Messages
Agent posts sensitive drafts to a supervisor via Slack. Approval can stall for hours when the supervisor is unavailable, delaying the customer notification. Time cost: 25 min/cycle.
5
Send the Message via the Correct Channel
Agent sends the approved message through Gmail, Intercom, or Twilio based on customer preference. Manual channel switching introduces risk of sending to the wrong tool. Time cost: 8 min/cycle.
6
Log the Communication in the CRM
Agent manually adds a timestamped note to the customer's HubSpot record. Frequently skipped under ticket load, leaving the CRM record incomplete. Time cost: 5 min/cycle.
7
Monitor for Customer Reply
Agent monitors the relevant inbox or chat thread for a response. Without a shared log, replies can sit unnoticed if the original sender is unavailable. Time cost: 10 min/cycle.
8
Update CRM with Reply Outcome
Agent records the thread outcome in HubSpot, usually in bulk at end of day, reducing record accuracy. Time cost: 7 min/cycle.
Time cost summary: Total manual time per cycle is 100 minutes across 8 steps. At approximately 214 qualifying events per month (~53/week) the gross exposure is roughly 6 hours of agent and supervisor time per week, costing $9,300/year at a $30/hour loaded rate. The automation replaces steps 1, 2, 3, 5, 6, and 8 entirely. Step 4 (supervisor approval) is reduced to a single Slack click for flagged messages only. Step 7 (reply monitoring) remains a human responsibility and is out of scope for this build.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Status Monitor Agent

Continuously watches HubSpot for contact, deal, or order records that move to any status value contained in the agreed notification ruleset. When a qualifying change is detected the agent fetches the full customer profile, retrieves the preferred communication channel from the HubSpot contact property (falling back to email if the field is blank or null), and assembles a structured data package for the Message Drafting Agent. This agent replaces the daily manual CRM scan and the Google Sheets preference lookup, eliminating the single largest source of missed notifications. Estimated build time: 6 hours. Complexity: Moderate.

Trigger
HubSpot webhook: contact, deal, or order record moves to a status value present in the notification ruleset. Webhook fires on property change event for the agreed property name (e.g. hs_pipeline_stage or custom order_status property).
Tools
HubSpot (webhook trigger, contact property read), Google Sheets (legacy preference fallback read, read-only scope)
Replaces steps
Step 1 (CRM scan), Step 2 (contact preference lookup)
Estimated build
6 hours, Moderate
// Input
HubSpot webhook payload {
  event_type: 'propertyChange',
  object_type: 'CONTACT' | 'DEAL' | 'TICKET',
  object_id: string,
  property_name: string,          // e.g. 'order_status'
  new_value: string,              // must match notification ruleset
  occurred_at: ISO8601 timestamp
}

// Output (structured data package to Message Drafting Agent)
customer_package {
  customer_id: string,            // HubSpot contact ID
  customer_name: string,          // firstname + lastname
  customer_email: string,
  customer_phone: string | null,
  preferred_channel: 'email' | 'sms' | 'intercom',  // defaults to 'email' if null
  status_type: string,            // matched ruleset key, e.g. 'order_shipped'
  status_display: string,         // human-readable label for message body
  object_id: string,
  object_type: string,
  context_fields: {               // any deal/ticket properties relevant to the template
    deal_name?: string,
    order_ref?: string,
    renewal_date?: string,
    assigned_agent?: string
  },
  sensitivity_hints: string[],    // pre-flagged tags from HubSpot, e.g. ['complaint','delay']
  occurred_at: ISO8601 timestamp
}
  • HubSpot tier must support webhook subscriptions (Marketing Hub Starter or above, or Operations Hub). Confirm before build begins.
  • The exact property name used as the status trigger (e.g. order_status, hs_pipeline_stage) must be agreed and documented in the rules configuration file before the webhook is registered.
  • The notification ruleset (the list of status values that fire the trigger) must be provided as a configuration array, not hardcoded. New statuses must be addable without a code change.
  • If the HubSpot contact property preferred_channel is blank, the agent defaults to 'email'. If the customer has no valid email address, the run must halt and log an error to the Supabase error table rather than attempting delivery.
  • The Google Sheets fallback (legacy preference store) is read-only. Match is on customer email address. If no match is found, proceed with HubSpot value or default. Do not write to the Sheet.
  • Dedupe: if the same object_id fires a second webhook within 5 minutes for the same property change (HubSpot can emit duplicate events), discard the duplicate by checking a short-lived run log keyed on object_id + property_name + occurred_at.
  • Confirm with the process owner which HubSpot object types are in scope at go-live: CONTACT, DEAL, and/or TICKET.
Message Drafting Agent

Receives the structured customer data package from the Status Monitor Agent and selects the correct pre-approved message template for the detected status type. It personalises the template by substituting the customer name, status display label, and any relevant context fields. After personalisation it evaluates the sensitivity ruleset: if any sensitivity condition is met (status type is in the sensitive list, or a sensitivity hint tag is present) the draft is posted to the designated Slack channel as a formatted approval message with Approve and Edit buttons. Non-sensitive drafts are passed directly to the Delivery and Logging Agent without human intervention. This agent replaces manual message drafting and routes the supervisor approval step to a structured, auditable Slack interface. Estimated build time: 10 hours. Complexity: Complex.

Trigger
Structured customer_package received from Status Monitor Agent via internal workflow queue.
Tools
HubSpot (template metadata lookup), Slack (approval message post, interactive button handler)
Replaces steps
Step 3 (message drafting), Step 4 (supervisor approval loop, reduced to one-click Slack action)
Estimated build
10 hours, Complex
// Input (from Status Monitor Agent)
customer_package { ... }  // full schema as above

// Processing
template_key = TEMPLATE_MAP[customer_package.status_type]
draft_body   = render(template_key, customer_package)   // Mustache-style token substitution
is_sensitive = SENSITIVITY_RULES.evaluate(customer_package)  // returns boolean

// Output: standard message (non-sensitive)
approved_message {
  customer_id: string,
  customer_name: string,
  preferred_channel: 'email' | 'sms' | 'intercom',
  customer_email: string,
  customer_phone: string | null,
  message_body: string,
  template_key: string,
  status_type: string,
  object_id: string,
  is_sensitive: false,
  approved_by: 'auto',
  approved_at: ISO8601 timestamp
}

// Output: sensitive message (requires approval)
slack_approval_post {
  channel: SLACK_APPROVAL_CHANNEL_ID,
  text: 'Sensitive update pending approval',
  blocks: [draft preview, Approve button, Edit button],
  metadata: { run_id: string, customer_id: string, draft_body: string }
}

// On approval (Slack interactive callback)
approved_message {
  ...same shape as standard output...,
  is_sensitive: true,
  approved_by: slack_user_id,
  approved_at: ISO8601 timestamp
}
  • Message templates must be stored in a configuration file or a simple key-value store (not hardcoded). Each template is keyed to a status_type string and contains Mustache-style tokens such as {{customer_name}}, {{status_display}}, {{order_ref}}.
  • The sensitivity ruleset is a separate configuration array listing: (a) status_type values that are always sensitive, and (b) sensitivity_hint tag values that trigger the flag. Both lists must be configurable without a code change.
  • The Slack approval interface must use Slack Block Kit interactive components. The Approve button payload must carry the full approved_message object so the Delivery Agent can proceed without a second data fetch.
  • If a supervisor clicks Edit, the flow must post a threaded reply prompting the supervisor to paste the revised message body. The revised text replaces draft_body before proceeding to delivery.
  • Slack approval requests must time out after 4 hours. On timeout, log the event to the Supabase error table with status 'approval_timeout' and alert the support manager via a separate Slack DM.
  • Confirm the SLACK_APPROVAL_CHANNEL_ID and the list of Slack user IDs authorised to approve messages before build. Do not allow approval from the original requesting system account.
  • HubSpot is listed as a tool for template metadata lookup only. If templates are stored externally, this dependency can be removed. Confirm storage location before build.
Delivery and Logging Agent

Receives an approved_message object (either auto-approved for standard messages or supervisor-approved for sensitive ones) and dispatches the message through the customer's preferred channel: Gmail for email, Twilio for SMS, or Intercom for in-app and chat. After a confirmed send, it writes a timestamped activity note to the customer's HubSpot record containing the message body, channel used, send time, and approver identity. It also accumulates sent events and posts an hourly digest to the support team's Slack channel so the team can see what customers have been told without querying the CRM. This agent replaces manual message sending, manual CRM logging, and end-of-day bulk record updates. Estimated build time: 10 hours. Complexity: Complex.

Trigger
approved_message object received from Message Drafting Agent (standard path) or from Slack interactive callback handler (sensitive approval path).
Tools
Gmail (email delivery), Twilio (SMS delivery), Intercom (in-app/chat delivery), HubSpot (activity note write-back), Slack (hourly digest post)
Replaces steps
Step 5 (send message), Step 6 (CRM logging), Step 8 (reply outcome logging, partial)
Estimated build
10 hours, Complex
// Input
approved_message {
  customer_id: string,
  customer_name: string,
  preferred_channel: 'email' | 'sms' | 'intercom',
  customer_email: string,
  customer_phone: string | null,
  message_body: string,
  template_key: string,
  status_type: string,
  object_id: string,
  is_sensitive: boolean,
  approved_by: 'auto' | slack_user_id,
  approved_at: ISO8601 timestamp
}

// Channel routing logic
if preferred_channel == 'email'    -> Gmail API: send via authenticated sender address
if preferred_channel == 'sms'      -> Twilio REST API: send to customer_phone
if preferred_channel == 'intercom' -> Intercom Messages API: send to contact by customer_email

// Output: HubSpot activity note
hubspot_note {
  object_id: string,
  note_body: string,   // 'Automated update sent via {channel} at {timestamp}. Template: {template_key}. Approved by: {approved_by}.',
  timestamp: ISO8601
}

// Output: Slack digest accumulator entry
digest_entry {
  customer_name: string,
  status_type: string,
  channel: string,
  sent_at: ISO8601,
  approved_by: string
}

// Digest post (hourly scheduled flush)
slack_digest_post {
  channel: SLACK_DIGEST_CHANNEL_ID,
  text: 'Hourly outbound summary: {N} messages sent',
  blocks: [summary table of digest_entry list]
}

// On delivery failure
error_log {
  run_id: string,
  customer_id: string,
  channel: string,
  error_code: string,
  error_message: string,
  failed_at: ISO8601,
  status: 'delivery_failed'
}
  • Gmail delivery must use an OAuth 2.0 authenticated sender account. The From address must be agreed with the process owner before build. Confirm the Gmail account has the gmail.send scope granted to the automation platform service account.
  • Twilio: use the Twilio Messages API (POST /2010-04-01/Accounts/{AccountSid}/Messages.json). Confirm the Twilio phone number, account SID, and auth token are available in the credential store before build. SMS requires a valid E.164-formatted customer_phone value. If customer_phone is null and preferred_channel is 'sms', halt and log an error.
  • Intercom: use the Intercom Messages API (POST /messages) with message_type 'outbound'. Match contact by customer_email using the Contacts search endpoint. Confirm the Intercom access token scope includes write access to messages.
  • HubSpot activity note write-back uses the Engagements API v2 (POST /engagements/v2/engagements) or the Notes API if on CRM API v3. Confirm which API version is available on the client's HubSpot subscription.
  • The hourly digest is triggered by a scheduled cron within the orchestration layer (every 60 minutes). The accumulator holding digest_entry records between flushes must be cleared after each successful post.
  • On any delivery failure (non-2xx response from Gmail, Twilio, or Intercom), write to the Supabase error log and send an immediate alert to SLACK_ALERT_CHANNEL_ID. Do not retry automatically more than twice with a 90-second back-off before escalating.
  • Confirm SLACK_DIGEST_CHANNEL_ID (team summary channel) is different from SLACK_APPROVAL_CHANNEL_ID (supervisor approval channel). Both IDs must be stored as environment variables, not hardcoded.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full data journey: trigger to output, with agent handoff comments and exact field names
// ════════════════════════════════════���══════════════════════════════
// TRIGGER: HubSpot property change webhook
// ═══════════════════════════════════════════════════════════════════
HubSpot webhook ->
  event_type:     'propertyChange'
  object_type:    'CONTACT' | 'DEAL' | 'TICKET'
  object_id:      '12345678'
  property_name:  'order_status'
  new_value:      'order_shipped'          // must be in NOTIFICATION_RULESET[]
  occurred_at:    '2024-06-09T10:32:00Z'

// ── DEDUPE CHECK ────────────────────────────────────────────────────
// Check run_log for duplicate: key = object_id + property_name + occurred_at
// If duplicate found -> discard and halt
// If new -> write to run_log and continue

// ═══════════════════════════════════════════════════════════════════
// STATUS MONITOR AGENT: fetch customer data from HubSpot + Google Sheets
// ═══════════════════════════════════════════════════════════════════
HubSpot GET /crm/v3/objects/contacts/{object_id}?properties=
  firstname, lastname, email, phone,
  preferred_channel, hs_pipeline_stage,
  deal_name, order_ref, renewal_date, assigned_agent,
  hs_tag_ids                               // used for sensitivity_hints

Google Sheets GET (fallback, read-only) ->
  match on: email
  retrieve: preferred_channel_override     // applied only if HubSpot field is null

// Build customer_package
customer_package {
  customer_id:      '12345678',
  customer_name:    'Jane Smith',
  customer_email:   'jane@example.com',
  customer_phone:   '+15550001234',
  preferred_channel:'email',               // resolved with fallback logic
  status_type:      'order_shipped',
  status_display:   'Your order has shipped',
  object_id:        '12345678',
  object_type:      'CONTACT',
  context_fields: {
    order_ref:      'ORD-9921',
    assigned_agent: 'Rachel Torres'
  },
  sensitivity_hints: [],                   // e.g. ['complaint'] if tag present
  occurred_at:      '2024-06-09T10:32:00Z'
}

// ── HANDOFF: Status Monitor Agent -> Message Drafting Agent ─────────

// ═══════════════════════════════════════════════════════════════════
// MESSAGE DRAFTING AGENT: template selection, personalisation, sensitivity check
// ═══════════════════════════════════════════════════════════════════
template_key = TEMPLATE_MAP['order_shipped']
  // -> 'Hi {{customer_name}}, great news — your order {{order_ref}} has shipped.'

draft_body = render(template_key, customer_package)
  // -> 'Hi Jane Smith, great news — your order ORD-9921 has shipped.'

is_sensitive = SENSITIVITY_RULES.evaluate({
  status_type:       'order_shipped',      // not in SENSITIVE_STATUS_LIST -> false
  sensitivity_hints: []                    // no hint tags -> false
})
// is_sensitive = false -> route to auto-approval

// ── PATH A: Non-sensitive (auto-approved) ───────────────────────────
approved_message {
  customer_id:      '12345678',
  customer_name:    'Jane Smith',
  preferred_channel:'email',
  customer_email:   'jane@example.com',
  customer_phone:   '+15550001234',
  message_body:     'Hi Jane Smith, great news — your order ORD-9921 has shipped.',
  template_key:     'order_shipped',
  status_type:      'order_shipped',
  object_id:        '12345678',
  is_sensitive:     false,
  approved_by:      'auto',
  approved_at:      '2024-06-09T10:32:05Z'
}

// ── PATH B: Sensitive (Slack approval required) ──────────────────────
// is_sensitive = true ->
Slack POST /chat.postMessage {
  channel: SLACK_APPROVAL_CHANNEL_ID,
  blocks: [draft preview, Approve button, Edit button],
  metadata: { run_id, customer_id, draft_body }
}
// Supervisor clicks Approve ->
// Slack interactive callback -> approved_message (is_sensitive: true, approved_by: slack_user_id)
// Timeout after 4 hours -> write Supabase error + DM support manager

// ── HANDOFF: Message Drafting Agent -> Delivery and Logging Agent ───

// ═══════════════════════════════════════════════════════════════════
// DELIVERY AND LOGGING AGENT: channel routing, send, log, digest
// ═══════════════════════════════════════════════════════════════════

// Channel routing (preferred_channel = 'email')
Gmail API POST /gmail/v1/users/me/messages/send {
  to:      customer_email,
  subject: status_display,
  body:    message_body
}
// -> send_confirmation { message_id, sent_at }

// (preferred_channel = 'sms')
Twilio POST /2010-04-01/Accounts/{SID}/Messages.json {
  To:   customer_phone,          // E.164 format
  From: TWILIO_FROM_NUMBER,
  Body: message_body
}
// -> send_confirmation { sid, status: 'queued' }

// (preferred_channel = 'intercom')
Intercom POST /messages {
  message_type: 'outbound',
  subject:      status_display,
  body:         message_body,
  from:         { type: 'admin', id: INTERCOM_ADMIN_ID },
  to:           { type: 'user', email: customer_email }
}
// -> send_confirmation { id, created_at }

// HubSpot activity note write-back
HubSpot POST /engagements/v2/engagements {
  engagement: { type: 'NOTE', timestamp: approved_at },
  associations: { contactIds: [customer_id] },
  metadata: {
    body: 'Automated update sent via email at 2024-06-09T10:32:05Z. Template: order_shipped. Approved by: auto.'
  }
}

// Accumulate digest entry (flushed hourly)
digest_entry {
  customer_name: 'Jane Smith',
  status_type:   'order_shipped',
  channel:       'email',
  sent_at:       '2024-06-09T10:32:07Z',
  approved_by:   'auto'
}

// Hourly cron: Slack digest post
Slack POST /chat.postMessage {
  channel: SLACK_DIGEST_CHANNEL_ID,
  text:    'Hourly outbound summary: 12 messages sent',
  blocks:  [summary table of digest entries]
}

// On any delivery failure ->
Supabase INSERT INTO error_log {
  run_id, customer_id, channel, error_code, error_message, failed_at, status: 'delivery_failed'
}
Slack POST to SLACK_ALERT_CHANNEL_ID -> 'Delivery failure: {customer_id} via {channel}'
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform (tool selection to be confirmed at build kickoff). One workflow per agent: Status Monitor workflow, Message Drafting workflow, Delivery and Logging workflow. All three workflows share a single credential store. Inter-workflow communication uses the platform's internal queue or a lightweight webhook call between workflows. No agent logic is embedded in a single monolithic workflow.
Webhook configuration
HubSpot webhook subscription registered via the HubSpot Webhooks API for the agreed property name (e.g. order_status) on the agreed object types (CONTACT, DEAL, TICKET). Webhook target URL is the orchestration platform's public inbound endpoint with a shared secret header for verification. The secret is stored as an environment variable (HUBSPOT_WEBHOOK_SECRET) and validated on every inbound request. Slack interactive component callbacks (approval button clicks) are routed to a dedicated inbound endpoint (SLACK_INTERACTIVITY_URL) registered in the Slack app manifest.
Templating approach
Message templates stored in a JSON configuration file (or a Supabase config table if runtime editability is required). Each entry is keyed by status_type and contains a plain-text body with Mustache-style tokens (e.g. {{customer_name}}, {{order_ref}}). Template rendering is handled by the orchestration platform's built-in expression engine or a lightweight Mustache library. The sensitivity ruleset and notification ruleset are stored in the same configuration structure. All three configuration objects must be editable by the process owner without a code deployment.
Error logging
All error events (delivery failures, approval timeouts, missing required fields, dedupe discards, API non-2xx responses) are written to a Supabase table: error_log (run_id UUID, customer_id text, channel text, error_code text, error_message text, failed_at timestamptz, status text, resolved_at timestamptz nullable). A Supabase database webhook or a scheduled query checks for unresolved errors older than 30 minutes and posts an alert to SLACK_ALERT_CHANNEL_ID. The support manager is the designated recipient for approval-timeout DMs.
Testing approach
All three agents are built and tested in a sandbox environment first using a HubSpot sandbox account and Twilio test credentials. The Status Monitor Agent is tested against historical HubSpot webhook payloads replayed through the inbound endpoint. The Message Drafting Agent is tested with fixture customer_package objects covering standard and sensitive scenarios. The Delivery and Logging Agent is tested with mock send endpoints before switching to live API credentials. End-to-end testing (trigger to Slack digest) is run across all three delivery channels and both approval paths before go-live.
Environment variables required
HUBSPOT_API_KEY, HUBSPOT_WEBHOOK_SECRET, HUBSPOT_PORTAL_ID, GOOGLE_SHEETS_ID, GOOGLE_SERVICE_ACCOUNT_JSON, SLACK_BOT_TOKEN, SLACK_APPROVAL_CHANNEL_ID, SLACK_DIGEST_CHANNEL_ID, SLACK_ALERT_CHANNEL_ID, SLACK_INTERACTIVITY_URL, GMAIL_OAUTH_CLIENT_ID, GMAIL_OAUTH_CLIENT_SECRET, GMAIL_REFRESH_TOKEN, GMAIL_FROM_ADDRESS, TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_FROM_NUMBER, INTERCOM_ACCESS_TOKEN, INTERCOM_ADMIN_ID, SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
Estimated total build time
Status Monitor Agent: 6 hours. Message Drafting Agent: 10 hours. Delivery and Logging Agent: 10 hours. End-to-end integration testing and environment setup: 2 hours. Total: 28 hours (aligns with the confirmed build effort figure).
Before any build work begins, the FullSpec team requires the following from your team: (1) the signed-off list of notification-eligible status values and the exact HubSpot property name, (2) the sensitivity flagging rules (which status types and which tag values trigger a human review), (3) confirmation that a data quality pass on the preferred_channel field in HubSpot has been completed or is scheduled, (4) the Slack channel IDs for approval, digest, and alerts, and (5) API credentials or confirmed access for HubSpot, Gmail, Twilio, and Intercom. Build cannot start on the Message Drafting Agent until (1) and (2) are confirmed. Build cannot start on the Delivery Agent until Gmail OAuth consent has been granted and Twilio credentials are available in the credential store.
Support contact
support@gofullspec.com
Process owner
[Your name]
Owner email
[Rep email]
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Proactive Customer Communication.

Launch Plan
Operations · Owner
View
ROI and Business Case
Finance · Owner
View
Process Runbook / SOP
Operations · Owner
View
Integration and API Spec
Technical · Developer
View
Test and QA Plan
Quality · Developer
View