Back to Lost Lead Re-engagement

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

Lost Lead Re-engagement

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

This document gives the FullSpec build team everything needed to implement the Lost Lead Re-engagement automation end to end. It covers the current-state process map with bottleneck flags, full agent specifications with IO contracts, the end-to-end data flow trace, and the recommended build stack. The FullSpec team is responsible for all build, configuration, and testing work described here. Your team's input is limited to approving AI-generated email drafts, confirming HubSpot API access, and reviewing the brand tone of sequence templates before go-live.

01Process snapshot

Step
Name
Description
1
Filter CRM for Inactive Leads
Sales rep opens HubSpot and manually builds a filter for contacts with no activity in 21 or more days who are not marked Won or Lost. Performed inconsistently and entirely from memory. Time cost: 35 min.
2
Export Lead List to Spreadsheet
Filtered list is exported to Google Sheets for manual prioritisation and note-taking. Frequently skipped, leading to no paper trail. Time cost: 15 min.
3
Research Each Lead Before Outreach
Rep reviews the contact's last interaction notes and deal history in HubSpot to personalise the email. Done inconsistently depending on workload. Time cost: 40 min.
4
Write Personalised Re-engagement Email
Rep drafts an individual email for each cold lead referencing the previous conversation. The single most time-consuming step in the cycle. Time cost: 60 min.
5
Send First Touch Email
Each email is sent individually from the rep's Gmail inbox with no scheduling or batching. Send times are random. Time cost: 20 min.
6
Log Outreach Activity in CRM
Rep manually logs the sent email as an activity on the HubSpot contact record. Frequently skipped under time pressure. Time cost: 15 min.
7
Set Manual Follow-up Reminder
Rep sets a HubSpot task or calendar reminder to check for a reply in five to seven days. If missed, the follow-up chain stops entirely. Time cost: 10 min.
8
Send Second or Third Touch Email
If no reply, rep sends another email, typically rewritten from scratch rather than following a planned sequence. Most reps abandon after one or two attempts. Time cost: 30 min.
9
Monitor Inbox for Replies
Rep checks Gmail manually for responses. Warm replies sit unread for hours alongside other email traffic. Time cost: 15 min.
10
Update Lead Status in CRM
If a reply is received, the rep updates the lifecycle stage in HubSpot. If no reply after all touches, the lead should be marked Lost but often is not. Time cost: 10 min.
Time cost summary: Total manual time per cycle is 250 minutes (approximately 4 hours 10 minutes). At 5 hours lost per week across the full lead volume, this costs $7,800 per year at a $30/hour loaded rate. Steps 1, 2 (daily scan and export), 3, 4, 5, 6, 7, 8 (research, draft, send, log, remind, follow-up), and 9, 10 (reply monitoring and CRM update) are all replaced by the three agents in this build. The one remaining human action is reviewing and responding to warm replies surfaced by the Reply Classifier and Router.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Lead Inactivity Monitor

Runs on a scheduled daily trigger at 7:00 AM. Queries the HubSpot Contacts API for all records where the last_activity_date property is more than 21 days in the past and where the lifecycle stage is not 'closedwon' or 'closedlost'. Each matching contact is passed downstream as an individual workflow run, carrying its full contact context payload. A corresponding row is written to the Google Sheets audit log on every trigger so that every initiated outreach is traceable from day one. Duplicate suppression is handled by checking a custom HubSpot property ('re_engagement_active') before queuing; if the property is set to true, the contact is skipped for that run.

Trigger
Scheduled cron: daily at 07:00 AM (business timezone). Runs regardless of weekend unless scoping confirms weekday-only is preferred.
Tools
HubSpot (Contacts API, custom property read/write), Google Sheets (audit log append)
Replaces steps
1 (CRM filter), 2 (spreadsheet export)
Estimated build
5 hours, Simple
// Input
HubSpot scheduled scan
  filter: last_activity_date < (today - 21 days)
  filter: lifecyclestage NOT IN ['closedwon', 'closedlost']
  filter: re_engagement_active != true

// Output (per contact, passed to Sequence Copywriter Agent)
{
  contact_id: string,          // HubSpot internal contact ID
  first_name: string,
  last_name: string,
  email: string,
  assigned_rep_email: string,  // hubspot_owner email
  last_activity_date: ISO8601,
  lifecycle_stage: string,
  deal_stage: string,
  last_note_body: string,      // most recent engagement note text
  company_name: string,
  touch_number: integer        // defaults to 1 on first run
}

// Google Sheets audit log row appended
[contact_id, email, run_timestamp, touch_number, status='queued']
  • HubSpot plan must include Contacts API access with custom property read and write. Confirm the CRM subscription tier supports workflow and property API before starting this build.
  • Custom property 're_engagement_active' (boolean) must be created in HubSpot before build. This is the primary dedupe guard: set to true when a sequence starts, cleared when the sequence ends (Closed Lost or warm reply received).
  • Custom property 're_engagement_touch_number' (integer, 1-3) must also be created to track sequence position across workflow runs.
  • Google Sheets audit log workbook must be created with columns: contact_id, email, triggered_at, touch_number, status, email_subject, sent_at, reply_received, final_outcome.
  • If the HubSpot owner (assigned rep) field is empty on a contact record, the workflow must route to a configurable fallback rep email. Confirm the fallback address with the process owner before build.
  • The 7:00 AM schedule timezone must be confirmed with the process owner and locked in the orchestration layer's environment config.
Sequence Copywriter Agent

Activated once per contact per sequence touch by the Lead Inactivity Monitor output (touch 1) and by a scheduled re-trigger if no reply is detected after the follow-up interval (touches 2 and 3, spaced five and ten days from the previous send respectively). Uses the full contact context payload to construct a structured prompt for OpenAI. The prompt instructs the model to write a short re-engagement email (120 to 180 words) referencing the specific last interaction, varying the angle across touches: touch 1 uses a value-lead angle, touch 2 uses a social proof or case study hook, and touch 3 uses a low-friction closing question. The drafted email is sent via Gmail from the assigned rep's OAuth-connected account, then logged as an engagement activity on the HubSpot contact record. The next follow-up date is set by writing to the 're_engagement_next_touch_date' custom property.

Trigger
Activated per contact by Lead Inactivity Monitor (touch 1) or by elapsed interval check with no reply flag (touches 2 and 3).
Tools
OpenAI (Chat Completions API, gpt-4o), HubSpot (activity log, custom property write), Gmail (send as assigned rep via OAuth 2.0)
Replaces steps
3 (lead research), 4 (email drafting), 5 (send first touch), 6 (CRM log), 7 (reminder), 8 (second and third touch)
Estimated build
10 hours, Moderate
// Input (from Lead Inactivity Monitor)
{
  contact_id: string,
  first_name: string,
  last_name: string,
  email: string,
  assigned_rep_email: string,
  last_note_body: string,
  company_name: string,
  touch_number: integer,       // 1, 2, or 3
  deal_stage: string
}

// OpenAI prompt construction (illustrative)
system: 'You are a sales assistant writing re-engagement emails on behalf of a rep.
         Keep the tone warm, direct, and concise (120-180 words).
         Touch 1: lead with value. Touch 2: social proof hook. Touch 3: low-friction close.'
user:   'Contact: {first_name} {last_name} at {company_name}.
         Last interaction note: {last_note_body}.
         Deal stage at last contact: {deal_stage}.
         This is touch number {touch_number}. Write the email now.'

// Output
{
  subject_line: string,
  email_body: string,
  sent_at: ISO8601,
  gmail_message_id: string,    // stored for reply thread matching
  hubspot_activity_id: string  // returned after logging
}

// HubSpot properties written after send
re_engagement_active: true
re_engagement_touch_number: {touch_number}
re_engagement_last_sent_at: {sent_at}
re_engagement_next_touch_date: {sent_at + interval_days}
re_engagement_gmail_thread_id: {gmail_thread_id}
  • OpenAI API key must be stored in the shared credential store. Use gpt-4o. Set max_tokens to 400 and temperature to 0.7 for this use case.
  • Gmail OAuth 2.0 must be configured with scopes: gmail.send and gmail.modify. One OAuth connection per rep account is required. Confirm all rep accounts before build.
  • Gmail sending rate: the orchestration layer must enforce a maximum of 80 sends per day per Gmail account to stay within Google Workspace sending limits. Implement a per-account counter in the workflow.
  • Each email send must capture and store the Gmail message_id and thread_id in HubSpot custom properties. These are required by the Reply Classifier for thread matching.
  • Touch intervals are: touch 2 is five days after touch 1 send date, touch 3 is ten days after touch 1 send date. Confirm these intervals with the process owner before building the re-trigger logic.
  • AI-generated email drafts for all three touch angles must be reviewed and approved by a senior rep during QA before the sequence goes live. Do not skip this step.
  • If OpenAI returns an error or empty response, the workflow must log the failure to the Google Sheets audit log and skip the send, not retry blindly. Alert the FullSpec monitoring channel.
Reply Classifier and Router

Monitors the Gmail inbox of each connected rep account for replies that match a tracked re-engagement thread ID. When a reply is detected, the full reply body is passed to OpenAI for intent classification. The model classifies the reply into one of four categories: interested (warm), neutral (non-committal), unsubscribe (opt-out), or out-of-office (auto-reply). Warm replies trigger an immediate Slack message to the assigned rep via a channel webhook, including the lead name, a one-sentence reply summary, and a direct HubSpot contact URL. Unsubscribe replies suppress the sequence by clearing 're_engagement_active' and setting a 'do_not_contact' flag in HubSpot. Out-of-office replies pause the sequence by adjusting the next touch date. When no reply is received after the third touch, the workflow updates the contact lifecycle stage to Closed Lost and clears all re-engagement properties.

Trigger
Gmail inbox poll (every 15 minutes) matching thread IDs stored from Sequence Copywriter sends, or sequence completion check after touch 3 interval elapses with no reply flag.
Tools
Gmail (inbox poll, gmail.readonly scope), OpenAI (Chat Completions API, gpt-4o), HubSpot (lifecycle stage update, property write), Slack (incoming webhook, rep channel)
Replaces steps
9 (inbox monitoring), 10 (CRM status update)
Estimated build
7 hours, Moderate
// Input: reply detected
{
  gmail_thread_id: string,     // matched against stored re_engagement_gmail_thread_id
  reply_body: string,
  reply_from_email: string,
  received_at: ISO8601,
  contact_id: string,          // looked up via thread_id match
  assigned_rep_email: string
}

// OpenAI classification prompt
system: 'Classify this email reply into exactly one of: interested, neutral, unsubscribe, out-of-office.'
user:   '{reply_body}'

// Output: intent = 'interested'
Slack webhook POST to rep channel:
{
  text: 'Warm reply from {first_name} {last_name} ({company_name}).
         Summary: {one_sentence_summary}.
         HubSpot: https://app.hubspot.com/contacts/{portal_id}/contact/{contact_id}'
}
HubSpot property writes:
  re_engagement_reply_intent: 'interested'
  re_engagement_active: false

// Output: intent = 'unsubscribe'
HubSpot property writes:
  re_engagement_active: false
  hs_email_optout: true
  re_engagement_reply_intent: 'unsubscribe'

// Output: intent = 'out-of-office'
HubSpot property writes:
  re_engagement_next_touch_date: {received_at + 7 days}

// Output: no reply after touch 3 (sequence complete)
HubSpot property writes:
  lifecyclestage: 'closedlost'
  re_engagement_active: false
  re_engagement_touch_number: 3
  re_engagement_final_outcome: 'no_reply'
  • Gmail inbox poll requires the gmail.readonly OAuth scope in addition to gmail.send and gmail.modify scoped to the Sequence Copywriter. Confirm all three scopes are granted per rep OAuth connection.
  • Thread ID matching is the primary dedupe mechanism for reply detection. If a thread ID is not found in HubSpot, the reply must be logged to the audit sheet and skipped, not misclassified.
  • The Slack webhook URL must be configured per rep or per shared sales channel, depending on team preference. Confirm routing preference with the process owner before build.
  • OpenAI classification must default to 'neutral' if the model returns an ambiguous or empty result. Neutral replies must not trigger a Slack alert or a Closed Lost update. They pause the sequence.
  • The 'hs_email_optout' property in HubSpot is a system field. Confirm the CRM plan allows writing to this property via API. Some lower-tier HubSpot plans restrict this.
  • Rate limit for Gmail inbox polling must not exceed one request per 15 minutes per connected account to avoid Google API quota exhaustion. Configure this interval explicitly in the orchestration layer.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical

03End-to-end data flow

Full trigger-to-output data flow across all three agents. Field names match HubSpot internal property names and Gmail API response keys.
// ============================================================
// LOST LEAD RE-ENGAGEMENT: END-TO-END DATA FLOW
// ============================================================

// TRIGGER
// Scheduled cron fires at 07:00 AM daily
// ============================================================
CRON_TRIGGER -> HubSpot Contacts API
  GET /crm/v3/objects/contacts
  filter: last_activity_date < (today - P21D)
  filter: lifecyclestage NOT IN ['closedwon', 'closedlost']
  filter: re_engagement_active != true
  returns: [ contact_id, first_name, last_name, email,
             hubspot_owner_id, lifecyclestage, last_activity_date ]

// --- AGENT HANDOFF 1: Lead Inactivity Monitor -> Sequence Copywriter Agent ---

// For each contact in result set:
HubSpot Contacts API -> fetch full record
  GET /crm/v3/objects/contacts/{contact_id}
  properties: [ first_name, last_name, email, company,
                deal_stage, hs_latest_meeting_notes,
                notes_last_updated, re_engagement_touch_number ]
  resolve owner: GET /crm/v3/owners/{hubspot_owner_id}
    returns: assigned_rep_email

HubSpot -> Google Sheets audit log
  APPEND row:
  [ contact_id, email, run_timestamp, touch_number=1, status='queued' ]

// Payload passed to Sequence Copywriter Agent
contact_payload = {
  contact_id, first_name, last_name, email, company_name,
  assigned_rep_email, last_note_body, deal_stage, touch_number
}

// ============================================================
// SEQUENCE COPYWRITER AGENT
// ============================================================

contact_payload -> OpenAI Chat Completions API
  POST /v1/chat/completions
  model: gpt-4o
  max_tokens: 400
  temperature: 0.7
  messages: [ system_prompt, user_prompt(contact_payload) ]
  returns: { subject_line, email_body }

{ subject_line, email_body } -> Gmail API
  POST /gmail/v1/users/{assigned_rep_email}/messages/send
  to: contact.email
  from: assigned_rep_email
  subject: subject_line
  body: email_body
  returns: { gmail_message_id, gmail_thread_id }

{ gmail_message_id, gmail_thread_id, sent_at } -> HubSpot
  POST /crm/v3/objects/engagements   // log email activity
    type: EMAIL
    associations: [ contact_id ]
    metadata: { subject: subject_line, body: email_body, sentAt: sent_at }
  PATCH /crm/v3/objects/contacts/{contact_id}
    re_engagement_active: true
    re_engagement_touch_number: touch_number
    re_engagement_last_sent_at: sent_at
    re_engagement_next_touch_date: sent_at + interval_days
    re_engagement_gmail_thread_id: gmail_thread_id

Google Sheets audit log
  UPDATE row: status='sent', email_subject=subject_line, sent_at=sent_at

// Re-trigger logic (touches 2 and 3):
// Orchestration layer polls HubSpot daily for contacts where:
//   re_engagement_active = true
//   re_engagement_next_touch_date <= today
//   re_engagement_reply_intent IS NULL
// If condition met, increment touch_number and re-enter Sequence Copywriter Agent

// --- AGENT HANDOFF 2: Sequence Copywriter Agent -> Reply Classifier and Router ---

// ============================================================
// REPLY CLASSIFIER AND ROUTER
// ============================================================

// Gmail inbox poll (every 15 minutes per rep account)
Gmail API
  GET /gmail/v1/users/{assigned_rep_email}/messages
  q: 'in:inbox newer_than:15m'
  for each message: GET /gmail/v1/users/{me}/messages/{message_id}
    extract: thread_id, from_email, reply_body, received_at
  match thread_id against HubSpot re_engagement_gmail_thread_id
  if no match: skip and log to audit sheet

reply_body -> OpenAI Chat Completions API
  POST /v1/chat/completions
  model: gpt-4o
  messages: [ classification_system_prompt, user_prompt(reply_body) ]
  returns: intent_classification // 'interested'|'neutral'|'unsubscribe'|'out-of-office'

// ROUTE: intent = 'interested'
intent='interested' -> Slack Incoming Webhook
  POST {slack_webhook_url}
  payload: { text: 'Warm reply: {first_name} {last_name} ({company_name}).
              Summary: {reply_summary}. Link: {hubspot_contact_url}' }
-> HubSpot PATCH /crm/v3/objects/contacts/{contact_id}
    re_engagement_reply_intent: 'interested'
    re_engagement_active: false

// ROUTE: intent = 'unsubscribe'
intent='unsubscribe' -> HubSpot PATCH
    re_engagement_active: false
    hs_email_optout: true
    re_engagement_reply_intent: 'unsubscribe'

// ROUTE: intent = 'out-of-office'
intent='out-of-office' -> HubSpot PATCH
    re_engagement_next_touch_date: received_at + P7D

// ROUTE: intent = 'neutral'
intent='neutral' -> no action, sequence continues on next touch date

// ROUTE: sequence complete, no reply (touch_number = 3, no reply after interval)
-> HubSpot PATCH /crm/v3/objects/contacts/{contact_id}
    lifecyclestage: 'closedlost'
    re_engagement_active: false
    re_engagement_final_outcome: 'no_reply'
-> Google Sheets audit log UPDATE: final_outcome='closed_lost'

// ============================================================
// END OF FLOW
// ============================================================
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation tool with native HTTP request support and credential management. One workflow per agent (three workflows total): Lead Inactivity Monitor, Sequence Copywriter Agent, Reply Classifier and Router. All API credentials stored in a shared credential store within the platform, never hardcoded in workflow nodes. Workflows version-controlled and exported as JSON for handover documentation.
Webhook configuration
No inbound webhooks are required for this build. All triggers are either scheduled cron (Lead Inactivity Monitor at 07:00 AM daily) or polling-based (Gmail inbox poll every 15 minutes via HTTP GET). The Slack alert uses an outbound Slack Incoming Webhook URL. Configure the Slack webhook URL as an environment variable in the credential store, not inline in the workflow.
Templating approach
OpenAI prompts are stored as parameterised string templates within the Sequence Copywriter Agent workflow. Each of the three touch prompts (value-lead, social proof, low-friction close) is a separate template node with variable substitution for first_name, company_name, last_note_body, deal_stage, and touch_number. Templates must be exported and stored in the project documentation folder for process owner review and version tracking. Do not embed prompt text directly in HTTP body fields without a reference comment.
Error logging
All workflow errors (API failures, empty OpenAI responses, unmatched thread IDs, Gmail send failures) are written to a Supabase table named 're_engagement_error_log' with columns: error_id (uuid), occurred_at (timestamp), workflow_name, contact_id, error_type, error_message, retry_eligible (boolean). A Slack alert is sent to the FullSpec monitoring channel (not the rep channel) on any error where retry_eligible is false. Retryable errors (transient API timeouts) trigger one automatic retry after a 60-second delay before logging to Supabase.
Testing approach
All three agents are built and tested in a sandbox environment first. HubSpot sandbox account with 20 synthetic contacts representing a range of inactivity dates, lifecycle stages, and owner assignments. Gmail sending tested against a dedicated test inbox, not a live rep account. OpenAI outputs reviewed against the approved prompt templates for all three touch angles and all four reply classification categories. Slack webhook tested with a mock warm reply payload. Only after full sandbox sign-off does FullSpec promote the workflow to the production credential store and live HubSpot portal.
Estimated total build time
Lead Inactivity Monitor: 5 hours (Simple). Sequence Copywriter Agent: 10 hours (Moderate). Reply Classifier and Router: 7 hours (Moderate). End-to-end integration testing and error handling: 5 hours. Total: 27 hours across a 3 to 4 week delivery window, accounting for QA cycles, email template review, and process owner approvals at each stage.
Before any build work begins, the following must be confirmed: HubSpot API access tier supports custom property reads and writes via the Contacts and Engagements APIs; Gmail OAuth 2.0 credentials are set up for every rep account that will send re-engagement emails; OpenAI API key is active with sufficient quota for the expected send volume (estimate 80 to 160 API calls per day); Slack Incoming Webhook URL is generated for the target rep or sales channel; custom HubSpot properties (re_engagement_active, re_engagement_touch_number, re_engagement_last_sent_at, re_engagement_next_touch_date, re_engagement_gmail_thread_id, re_engagement_reply_intent, re_engagement_final_outcome) are created before the first workflow is deployed. Contact support@gofullspec.com if any of these prerequisites are blocked.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Lost Lead Re-engagement.

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