Back to Churn Risk Identification

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

Churn Risk Identification

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

This document gives the FullSpec build team everything needed to configure, connect, and deploy the Churn Risk Identification automation. It covers the current manual process step map, both agent specifications with IO contracts, the full end-to-end data flow, and the recommended build stack. Where the process template does not specify an exact field name or API detail, this document supplies realistic, internally consistent values that align with the named tools. The process owner's responsibilities are limited to approving outreach emails and confirming scoring thresholds before build begins. Everything else is handled by FullSpec.

01Process snapshot

Step
Name
Description
1
Pull Weekly Usage Report
CSM exports a login-frequency and event-volume report from Mixpanel and saves it to a shared Google Sheet. No automated filter is applied; the rep decides which accounts to include. Time cost: 25 min/cycle.
2
Check Open Support Tickets
Support rep opens Intercom and manually scans every account for tickets open longer than five days or with more than three submissions in the past fortnight. No saved filter is used consistently. Time cost: 30 min/cycle.
3
Review CRM Account Health Notes
CSM reads HubSpot contact and deal records for recent notes, last contact date, and manually entered health flags. There is no standard field, so information quality varies by rep. Time cost: 35 min/cycle.
4
Compile Risk List in Spreadsheet
CSM copies account names, signals, and a subjective priority score into a shared Google Sheet. The scoring method is not documented and changes between reviewers. Time cost: 40 min/cycle.
5
Hold Internal Triage Meeting
Support lead and CSM team meet to agree which accounts need urgent outreach and who owns each one. Outcome is verbal; notes are rarely written up. Time cost: 30 min/cycle.
6
Update CRM with Risk Status
Support rep manually updates each flagged HubSpot record with a risk label and assigned owner. Labels are free-text with no controlled vocabulary. Time cost: 20 min/cycle.
7
Draft and Send Outreach Email
Assigned CSM writes a personalised check-in email in Gmail, sometimes from a saved draft and often from scratch. No template is enforced. Time cost: 45 min/cycle.
8
Log Outreach Activity in CRM
Rep returns to HubSpot, logs the email as an activity, adds a follow-up task, and notes the expected response window. Time cost: 15 min/cycle.
9
Post Alert to Team Slack Channel
Support lead manually composes and posts a summary of at-risk accounts to the team Slack channel. Time cost: 10 min/cycle.
10
Monitor Customer Response
CSM checks Gmail and Intercom over following days for replies and manually updates the Google Sheet with outcomes. Time cost: 20 min/cycle.
Time cost summary: Total manual time per cycle is 270 minutes (4.5 hours) for a single daily review pass. Across a five-day working week the process runs daily, but the full review is conducted once per week, giving 270 minutes/week of direct labour on this process alone. The ROI baseline uses 6 hours/week (360 min), which includes ad hoc follow-up and context-switching overhead not captured in the step map. Steps 1, 2, 3, 4, 6, 7, 8, and 9 are fully replaced by the two agents. Steps 5 and 10 are restructured: step 5 (triage meeting) is replaced by the Slack alert, and step 10 (monitoring) is out of scope for this automation but benefits from better CRM logging.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Churn Signal Aggregator

This agent runs on a fixed daily schedule and is responsible for collecting raw customer signals from three source systems, Mixpanel for product usage events, Intercom for support ticket data, and HubSpot for CRM account health fields. It normalises all three data sets into a single account snapshot object per active customer, resolving identity across systems using the HubSpot company ID as the canonical key. The output is a structured JSON array passed directly to the Risk Scoring and Routing Agent. No scoring or routing logic lives in this agent; its only concern is data fidelity and completeness. Estimated build time: 10 hours. Complexity: Moderate.

Trigger
Daily schedule, configured time each morning (recommended 06:00 in the account's local timezone). Fired by the orchestration layer's built-in cron scheduler.
Tools
Mixpanel (Data Export API), Intercom (Conversations and Contacts REST API), HubSpot (Companies and Contacts API v3)
Replaces steps
Steps 1, 2, and 3 (Pull Usage Report, Check Open Tickets, Review CRM Health Notes)
Estimated build
10 hours / Moderate
// Input
trigger: { type: 'schedule', time: '06:00', timezone: 'account_local' }
source_accounts: HubSpot Companies API -> filter: { lifecycle_stage: 'customer', is_active: true }

// Per account: Mixpanel fetch
mixpanel.export({ event: ['Login', 'Feature Used', 'Session Start'],
  from_date: today-30d, to_date: today,
  where: 'properties["$company_id"] == account.hubspot_company_id' })
-> fields: { login_count_7d, login_count_30d, feature_events_30d, last_login_date }

// Per account: Intercom fetch
intercom.conversations.list({ company_id: account.intercom_company_id,
  filter: { state: 'open', created_at_gt: today-14d } })
-> fields: { open_ticket_count, tickets_gt_5d_old, total_tickets_30d, last_ticket_date }

// Per account: HubSpot fetch
hubspot.companies.get(account.hubspot_company_id)
-> fields: { hs_last_activity_date, hs_analytics_last_visit_timestamp,
             custom_health_score, csm_owner_id, account_name, mrr }

// Output (one object per account, passed to Risk Scoring Agent)
{
  hubspot_company_id: string,
  account_name: string,
  csm_owner_id: string,
  mrr: number,
  login_count_7d: number,
  login_count_30d: number,
  feature_events_30d: number,
  last_login_date: ISO8601,
  open_ticket_count: number,
  tickets_gt_5d_old: number,
  total_tickets_30d: number,
  last_ticket_date: ISO8601,
  hs_last_activity_date: ISO8601,
  custom_health_score: number | null,
  snapshot_timestamp: ISO8601
}
  • Mixpanel requires a Service Account credential (username + secret) scoped to the relevant project. The project ID must be confirmed before build. The Data Export API is available on Growth plan and above; confirm the account tier before connecting.
  • Intercom API access requires an OAuth 2.0 app or an API key with read access to Conversations and Companies. Confirm that the workspace plan includes the Conversations API (available on Pro and above).
  • HubSpot connection requires a Private App token with scopes: crm.objects.companies.read, crm.objects.contacts.read, crm.objects.owners.read. The custom health score property internal name must be confirmed during the field mapping session before build.
  • Cross-system identity resolution: use HubSpot company ID as canonical key. Intercom company ID and Mixpanel $company_id must be mapped to HubSpot company ID in a lookup table configured at setup. Any account missing a cross-system ID is written to an error log and skipped; do not fail the entire run.
  • If Mixpanel returns zero events for an account, treat as login_count_7d = 0 (a valid churn signal), not as a missing data error.
  • Dedupe: if the daily run fires twice due to scheduler error, check snapshot_timestamp against the last successful run stored in the error log table. Skip if a snapshot for the same calendar day already exists.
  • Confirm with the process owner the exact Mixpanel event names used for login and feature engagement before writing the export query.
Risk Scoring and Routing Agent

This agent receives the normalised account snapshot array from the Churn Signal Aggregator and applies a configurable weighted scoring model to assign each account a risk tier of High, Medium, or Low. For every account scored High, it drafts a personalised outreach email using account-specific field values, prepares a structured Slack alert payload, updates the HubSpot company record with the risk tier and triggering signals, and posts the Slack alert with the draft email body linked or embedded for CSM review. Once a CSM approves the draft via the Slack interactive button, the agent sends the email through Gmail and then logs the activity and creates a follow-up task in HubSpot. Medium and Low accounts receive only the HubSpot risk tier update; no email or Slack alert is generated for them. Estimated build time: 16 hours. Complexity: Moderate.

Trigger
Fires immediately after the Churn Signal Aggregator delivers a completed account snapshot array. Passed via an internal workflow hand-off event, not an external webhook.
Tools
HubSpot (Companies and Engagements API v3), Slack (Web API: chat.postMessage, interactive components), Gmail (Gmail API: users.messages.send via OAuth 2.0)
Replaces steps
Steps 4, 6, 7, 8, and 9 (Compile Risk List, Update CRM, Draft Email, Log Activity, Post Slack Alert)
Estimated build
16 hours / Moderate
// Input
account_snapshot_array: AccountSnapshot[]  // from Churn Signal Aggregator

// Scoring model (configurable thresholds, confirm values before build)
score_signals: {
  login_drop_pct: (login_count_7d < login_count_30d * 0.4) -> +3,
  open_tickets_high: (open_ticket_count >= 3) -> +2,
  ticket_age_flag: (tickets_gt_5d_old >= 1) -> +2,
  no_recent_activity: (days_since(hs_last_activity_date) > 14) -> +2,
  health_score_low: (custom_health_score != null && custom_health_score < 40) -> +3
}
tier_thresholds: { high: score >= 5, medium: score 2-4, low: score <= 1 }

// Output: HubSpot update (all tiers)
hubspot.companies.update(hubspot_company_id, {
  churn_risk_tier: 'High' | 'Medium' | 'Low',
  churn_risk_signals: string[],
  churn_risk_score: number,
  churn_risk_last_updated: ISO8601
})

// Output: Slack alert (High tier only)
slack.chat.postMessage({
  channel: '#churn-risk-alerts',
  blocks: [
    { type: 'header', text: 'High-Risk Account: {account_name}' },
    { type: 'section', fields: [score, signals, mrr, csm_owner] },
    { type: 'actions', elements: [
      { type: 'button', text: 'Approve Email', action_id: 'approve_outreach' },
      { type: 'button', text: 'Edit Before Sending', action_id: 'edit_outreach' }
    ]}
  ],
  metadata: { hubspot_company_id, draft_email_body, csm_owner_id }
})

// On approval (Slack interactive callback -> action_id: 'approve_outreach')
gmail.users.messages.send({
  to: account.primary_contact_email,
  from: csm_owner_gmail_address,
  subject: 'Checking in, {account_name}',
  body: draft_email_body  // populated with account_name, csm_owner_name, signals summary
})
hubspot.engagements.create({
  type: 'EMAIL',
  hubspot_company_id,
  body: draft_email_body,
  timestamp: ISO8601
})
hubspot.tasks.create({
  hubspot_company_id,
  title: 'Follow up: churn risk outreach',
  due_date: today + (risk_tier == 'High' ? 3 : 7),
  owner_id: csm_owner_id
})
  • Scoring thresholds (login drop percentage, ticket count, health score cutoff) must be confirmed with the process owner before build. Use the values in the IO block as defaults. Document the agreed thresholds in a config file so they can be updated without a code change.
  • The Slack interactive component (approval button) requires the automation platform to expose a public HTTPS webhook endpoint that Slack can POST the callback to. Confirm the platform's webhook URL strategy before configuring the Slack app.
  • Gmail send-as permissions: the automation must send email as the assigned CSM, not a generic inbox. Each CSM must grant OAuth 2.0 delegated send access or the team must use a shared alias. Confirm this with the process owner before build. Scope required: gmail.send.
  • HubSpot custom properties (churn_risk_tier, churn_risk_signals, churn_risk_score, churn_risk_last_updated) must be created in the HubSpot account before the integration is built. Confirm property internal names after creation.
  • If Slack callback is not received within 48 hours of posting the alert, the automation should log the account as 'pending approval timeout' in the error log table and re-post a reminder to the same thread.
  • Email draft generation uses a fixed template with variable substitution. No LLM call is required unless the process owner requests personalisation beyond account name and signal summary. Confirm scope before build.
  • Medium and Low tier accounts must not receive a Slack alert or email draft. Only the HubSpot property update runs for those tiers. Build a conditional branch explicitly for this, not a filter on the Slack step.
  • The primary contact email for Gmail outreach is pulled from the HubSpot contact associated with the company record. If no primary contact exists, log the account as an exception and skip the email step without failing the workflow.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow, Churn Risk Identification automation
// ────────────────────���────────────────────────────────────────
// TRIGGER
// ─────────────────────────────────────────────────────────────
schedule.cron('06:00', timezone='account_local')
  -> emit: { run_id: uuid, triggered_at: ISO8601 }

// ─────────────────────────────────────────────────────────────
// AGENT 1: Churn Signal Aggregator
// ─────────────────────────────────────────────────────────────

// Step 1a: Fetch active customer accounts from HubSpot
HubSpot.Companies.list({ filter: { lifecycle_stage: 'customer', is_active: true } })
  -> accounts[]: { hubspot_company_id, account_name, csm_owner_id, mrr,
                   hs_last_activity_date, custom_health_score }

// Step 1b: For each account, fetch Mixpanel usage signals (parallel)
Mixpanel.DataExport.query({
  event: ['Login', 'Feature Used', 'Session Start'],
  from_date: today-30d, to_date: today,
  where: 'properties["$company_id"] == hubspot_company_id'
})
  -> usage: { login_count_7d, login_count_30d, feature_events_30d, last_login_date }

// Step 1c: For each account, fetch Intercom ticket signals (parallel)
Intercom.Conversations.list({
  company_id: intercom_company_id,
  filter: { state: 'open', created_at_gt: today-14d }
})
  -> tickets: { open_ticket_count, tickets_gt_5d_old, total_tickets_30d, last_ticket_date }

// Step 1d: Merge and normalise into account snapshot
merge(accounts[], usage, tickets)
  -> snapshot: AccountSnapshot {
       hubspot_company_id, account_name, csm_owner_id, mrr,
       login_count_7d, login_count_30d, feature_events_30d, last_login_date,
       open_ticket_count, tickets_gt_5d_old, total_tickets_30d, last_ticket_date,
       hs_last_activity_date, custom_health_score, snapshot_timestamp
     }

// Exception: if cross-system ID missing -> write to error_log, skip account
// Exception: if Mixpanel returns 0 events -> login_count_7d = 0 (valid signal)
// Exception: if duplicate run detected (snapshot_timestamp = today) -> halt

// ── HANDOFF: Agent 1 -> Agent 2 ──────────────────────────────
emit: { run_id, snapshots: AccountSnapshot[], account_count: number }

// ─────────────────────────────────────────────────────────────
// AGENT 2: Risk Scoring and Routing Agent
// ─────────────────────────────────────────────────────────────

// Step 2a: Score each account
for each snapshot in snapshots:
  score = 0
  signals = []
  if (login_count_7d < login_count_30d * 0.4):  score += 3; signals.push('login_drop')
  if (open_ticket_count >= 3):                   score += 2; signals.push('high_ticket_volume')
  if (tickets_gt_5d_old >= 1):                   score += 2; signals.push('aged_ticket')
  if (days_since(hs_last_activity_date) > 14):   score += 2; signals.push('no_crm_activity')
  if (custom_health_score != null
      && custom_health_score < 40):               score += 3; signals.push('low_health_score')
  tier = score >= 5 ? 'High' : score >= 2 ? 'Medium' : 'Low'
  -> scored_account: { ...snapshot, score, signals, tier }

// Step 2b: Write risk tier to HubSpot (all tiers)
HubSpot.Companies.update(hubspot_company_id, {
  churn_risk_tier: tier,
  churn_risk_signals: signals[],
  churn_risk_score: score,
  churn_risk_last_updated: ISO8601
})
  -> hubspot_update_status: 'success' | 'error'

// Step 2c: High-tier accounts only — draft outreach email
if tier == 'High':
  draft_email_body = template.render('churn_outreach_v1', {
    account_name, csm_owner_name, signals_summary, mrr
  })
  primary_contact_email = HubSpot.Contacts.getPrimary(hubspot_company_id)
    -> { contact_email, contact_first_name }

// Step 2d: Post Slack alert with approval buttons (High tier only)
  Slack.chat.postMessage({
    channel: '#churn-risk-alerts',
    blocks: [header, signal_summary, draft_preview, approve_button, edit_button],
    metadata: { hubspot_company_id, draft_email_body,
                csm_owner_id, contact_email, run_id }
  })
  -> slack_message_ts: string  // stored for thread reply on approval

// ── WAIT STATE: Slack interactive callback ────────────────────
// Webhook receives POST from Slack: { action_id, payload.metadata }
// Timeout: 48h -> log 'pending_approval_timeout', re-post reminder

// Step 2e: On approval (action_id == 'approve_outreach')
Gmail.users.messages.send({
  userId: csm_owner_gmail_address,
  to: contact_email,
  subject: 'Checking in, {account_name}',
  body: draft_email_body
})
  -> gmail_message_id: string

// Step 2f: Log email activity in HubSpot
HubSpot.Engagements.create({
  type: 'EMAIL',
  associations: [{ type: 'COMPANY', id: hubspot_company_id }],
  metadata: { subject, body: draft_email_body, from: csm_owner_gmail_address,
               to: contact_email, timestamp: ISO8601 }
})

// Step 2g: Create follow-up task in HubSpot
HubSpot.Tasks.create({
  title: 'Follow up: churn risk outreach',
  associations: [{ type: 'COMPANY', id: hubspot_company_id }],
  owner_id: csm_owner_id,
  due_date: today + 3,  // 3 days for High tier
  priority: 'HIGH'
})

// Step 2h: Post completion reply to Slack thread
Slack.chat.postMessage({
  channel: '#churn-risk-alerts',
  thread_ts: slack_message_ts,
  text: 'Email sent to {contact_email}. Follow-up task created in HubSpot.'
})

// ─────────────────────────────────────────────────────────────
// TERMINAL STATE
// ─────────────────────────────────────────────────────────────
// All accounts processed: risk tiers written to HubSpot
// High-risk accounts: Slack alert posted, email sent on approval,
//   HubSpot engagement logged, follow-up task created
// Errors: written to error_log table with run_id, account_id, error_type
// Run summary: written to run_log table { run_id, triggered_at, account_count,
//   high_count, medium_count, low_count, error_count, completed_at }
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform configured with one workflow per agent: 'CSA_Daily_Aggregator' and 'RSRA_Scoring_Routing'. Both workflows share a single credential store configured at the platform level. Agent 1 triggers Agent 2 via an internal workflow call (not an external HTTP request), keeping credentials isolated to the platform and avoiding unnecessary network hops.
Webhook configuration
One inbound HTTPS webhook endpoint required for the Slack interactive component callback (CSM approval). The endpoint must be publicly reachable and registered in the Slack app's Interactivity settings. The platform should verify the Slack request signature (X-Slack-Signature header, HMAC-SHA256) on every inbound POST. A second webhook may be used to expose a manual re-trigger endpoint for testing; protect with a shared secret header.
Templating approach
Outreach email bodies use a server-side template file ('churn_outreach_v1.html') with variable slots for account_name, csm_owner_name, signals_summary, and mrr. The template file is version-controlled and can be updated by the FullSpec team without touching workflow logic. Slack block-kit message layout is defined as a JSON template file ('slack_churn_alert_v1.json') and rendered at runtime with the same variable injection pattern.
Error logging
A Supabase PostgreSQL table ('churn_automation_errors') captures every exception with columns: run_id (uuid), triggered_at (timestamptz), hubspot_company_id (text), error_type (text), error_detail (text), resolved (boolean). A second table ('churn_run_log') records per-run summaries. A Slack alert to '#automation-errors' fires automatically when error_count > 0 in a run or when any single step returns a non-2xx response after two retries. FullSpec monitors this channel during the first 30 days post-launch.
Testing approach
All integrations are built and validated in sandbox environments first: HubSpot sandbox account, Mixpanel test project, Intercom test workspace, and Slack test workspace. Gmail send-as is tested against an internal FullSpec address before live CSM accounts are connected. Scoring logic is validated against a set of at least 20 anonymised account snapshots covering all tier outcomes (High, Medium, Low) and edge cases (missing health score, zero Mixpanel events, no primary contact). The full end-to-end flow is run in staging against live data in read-only mode before any write operations are enabled on production systems.
Credential store entries
MIXPANEL_SERVICE_ACCOUNT_USERNAME, MIXPANEL_SERVICE_ACCOUNT_SECRET, MIXPANEL_PROJECT_ID, INTERCOM_API_KEY, HUBSPOT_PRIVATE_APP_TOKEN, SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, GMAIL_OAUTH_CLIENT_ID, GMAIL_OAUTH_CLIENT_SECRET, GMAIL_OAUTH_REFRESH_TOKEN (one per CSM), SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
Estimated total build time
Churn Signal Aggregator: 10 hours. Risk Scoring and Routing Agent: 16 hours. End-to-end integration testing and staging validation: 2 hours. Total: 28 hours. This matches the confirmed build_effort_hours in the process template.
Pre-build blockers: three items must be resolved before any build work begins. First, the Mixpanel event names for login and feature engagement must be confirmed by the process owner; these are account-specific and cannot be assumed. Second, HubSpot custom properties (churn_risk_tier, churn_risk_signals, churn_risk_score, churn_risk_last_updated) must be created in the production HubSpot account and internal property names confirmed. Third, the scoring thresholds (login drop percentage, ticket count cutoffs, health score floor, no-activity window) must be agreed and signed off before the scoring model is coded. Contact support@gofullspec.com to schedule the pre-build mapping session.
The CSM email approval step is a deliberate design decision and must not be removed or bypassed. Automated outreach sent without human review to a customer considering cancellation carries a real risk of accelerating churn rather than preventing it. The Slack approval interaction is the only human touchpoint in the automated flow and should be treated as a non-negotiable constraint in the build.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Churn Risk Identification.

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