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
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
02Agent specifications
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.
// 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.
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.
// 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.
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.
// 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.
03End-to-end data flow
// ============================================================
// 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
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Lost Lead Re-engagement.