Developer Handover Pack
Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.
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
02Agent specifications
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.
// 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.
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.
// 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.
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.
// 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.
03End-to-end data flow
// ════════════════════════════════════���══════════════════════════════
// 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}'04Recommended build stack
More documents for this process
Every document generated for Proactive Customer Communication.