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
Outbound Prospecting & Cold Outreach Cadences
[YourCompany.com] · Sales Department · Prepared by FullSpec · [Today's Date]
This document hands over every technical detail the FullSpec team needs to build, configure, and connect the three-agent outbound prospecting automation for [YourCompany.com]. It covers the current-state step map, all agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. The process owner's role during this phase is to confirm API credentials and review the pilot results; everything on the build side is handled by FullSpec.
01Process snapshot
02Agent specifications
This agent fires the moment a HubSpot contact record is created or updated with the lifecycle stage property set to 'To Contact'. It calls the Clearbit Enrichment API and the Apollo.io People and Organization APIs in parallel, merges the responses, and writes the resulting structured data to a defined set of HubSpot custom contact properties. If Clearbit returns a full record, Apollo.io is used as a supplementary source for any empty fields only. If both sources return sparse data (fewer than three enrichment fields populated), the agent writes a flag property 'Enrichment Status: Sparse' to the record and creates a HubSpot task notifying the rep to enrich manually before the Personalisation Drafting Agent runs. Build complexity: Moderate. Estimated build time: 10 hours.
// Input hubspot_contact_id: string // HubSpot internal contact ID contact_email: string // Primary email address on the record contact_first_name: string // From HubSpot record at trigger time contact_last_name: string // From HubSpot record at trigger time company_domain: string // Derived from email domain or HubSpot company field // Output (written to HubSpot custom contact properties) hs_enriched_job_title: string // e.g. 'Head of Sales' hs_enriched_company_size: string // e.g. '51-200 employees' hs_enriched_tech_stack: string // comma-separated, e.g. 'Salesforce, Outreach, Gong' hs_enriched_trigger_event: string // e.g. 'Series B announced May 2024' hs_enrichment_source: string // 'Clearbit' | 'Apollo.io' | 'Both' | 'Sparse' hs_enrichment_status: string // 'Complete' | 'Sparse' | 'Failed' hs_enrichment_timestamp: datetime // ISO 8601, UTC
- HubSpot Sales Hub Professional or Enterprise seat is required to use native sequences. If the account is on Starter, the enrolment logic for the Cadence Execution Agent must be rebuilt inside the automation platform itself using scheduled Gmail sends.
- Clearbit API key must be provisioned before build starts. Confirm the Clearbit plan covers Enrichment (not just Reveal) so that person-level data is accessible.
- Apollo.io API requires a Basic plan or above for People API access. Confirm the active plan tier before configuring the connector.
- Dedupe logic: before writing any enriched field, the agent must check whether the field already contains a non-null value in HubSpot. Existing values are not overwritten; they are preserved and the new source value is appended to a log field 'hs_enrichment_raw_payload' for audit purposes.
- Fallback behaviour on API timeout: if either API fails to respond within 8 seconds, the agent retries once after a 5-second wait. On second failure, it sets 'hs_enrichment_status: Failed', creates a rep task, and halts the downstream chain for that contact.
- The Clearbit Enrichment API rate limit is 600 requests per minute on standard plans. At 120 prospects per month the average rate is well within limits, but burst imports (e.g. a list of 80 contacts imported at once) must be throttled to 50 requests per minute by the automation platform to avoid 429 errors.
This agent triggers immediately after the Prospect Enrichment Agent completes and writes 'hs_enrichment_status: Complete' to the HubSpot record. It reads the enriched custom fields, constructs a structured prompt, and calls an LLM (GPT-4o via the OpenAI Chat Completions API) to generate a single concise opening line (maximum 30 words) tailored to the prospect's role, company context, or trigger event. The generated line is written to the HubSpot custom contact property 'hs_personalisation_draft'. A HubSpot task is then created and assigned to the owning rep with a due date of the same business day, prompting them to approve or edit the draft before touch one is sent. The agent does not send any email. It stops after task creation. Build complexity: Moderate. Estimated build time: 9 hours.
// Input (read from HubSpot contact properties)
hubspot_contact_id: string
hs_enriched_job_title: string
hs_enriched_company_size: string
hs_enriched_tech_stack: string
hs_enriched_trigger_event: string
contact_first_name: string
company_name: string
// Prompt construction (system + user message sent to OpenAI)
system_prompt: 'You are an SDR writing concise, human-sounding cold email openers...'
user_prompt: 'Write one opening line (max 30 words) for {first_name} at {company}...'
// Output (written to HubSpot contact properties + task created)
hs_personalisation_draft: string // LLM-generated opening line, max 30 words
hs_draft_status: string // Set to 'Pending Review'
hs_task_id: string // ID of the HubSpot review task created
hs_draft_generated_at: datetime // ISO 8601, UTC
// On approval (rep action; triggers Cadence Execution Agent)
hs_draft_status: string // Updated to 'Approved' or 'Edited and Approved'
hs_approved_personalisation: string // Final line used in touch one (may differ from draft)- The prompt template must be configured during build with the business's specific tone, offer context, and any prohibited phrases. This requires a 30-minute briefing with the process owner before the agent is built.
- If 'hs_enrichment_status' is 'Sparse', this agent must not run automatically. A separate branch in the workflow should create a manual task for the rep to write the opening line, then set 'hs_draft_status: Manually Written' and 'hs_approved_personalisation' from the rep's input before handing off to the Cadence Execution Agent.
- OpenAI API key must be stored in the shared credential store, not hardcoded. Use environment variable 'OPENAI_API_KEY'.
- The maximum token budget for the combined prompt and completion should be set to 300 tokens to prevent runaway API costs at volume.
- HubSpot task must be assigned to the contact's 'hubspot_owner_id' field value, not a hardcoded rep ID, so multi-rep teams are supported from day one.
- If the rep does not action the task within 48 business hours, a second Slack nudge must fire to the rep's Slack user ID (stored as a custom HubSpot user property 'slack_user_id').
This agent takes over the moment a rep marks the personalisation draft as approved inside HubSpot, setting 'hs_draft_status' to 'Approved' or 'Edited and Approved'. It enrols the contact in the pre-configured HubSpot email sequence, injects the approved personalisation line into the touch one template via HubSpot's sequence enrolment API, and then monitors all subsequent touch events (sends, opens, clicks, replies, bounces) via HubSpot webhooks. Every event is logged to the contact timeline automatically. When a reply or meeting booking is detected, a Slack message is posted to the owning rep's channel with the contact name, company, last touch number, and a direct deep link to the HubSpot contact record. The agent also handles cadence completion: contacts who exhaust all touches without a reply are property-stamped 'hs_cadence_outcome: No Response' and removed from active task queues. Build complexity: Complex. Estimated build time: 14 hours.
// Input
hubspot_contact_id: string
hubspot_owner_id: string // Used to post Slack alert to correct rep
hs_approved_personalisation: string // Injected into touch one template token
hs_sequence_id: string // ID of the HubSpot sequence to enrol into
slack_user_id: string // From HubSpot user property, used for DM
// On each touch event (received via HubSpot webhook)
event_type: string // 'EMAIL_SENT' | 'EMAIL_OPENED' | 'EMAIL_CLICKED'
// | 'EMAIL_REPLIED' | 'EMAIL_BOUNCED' | 'MEETING_BOOKED'
touch_number: integer // 1 through 7
event_timestamp: datetime // ISO 8601, UTC
contact_email: string
// Output (written to HubSpot timeline + contact properties)
hs_last_touch_number: integer // Updated after each send
hs_last_touch_date: datetime
hs_cadence_status: string // 'Active' | 'Replied' | 'Booked' | 'No Response'
hs_cadence_outcome: string // Set on sequence completion
hs_total_touches_sent: integer
// On reply or booking (Slack alert payload)
slack_message_text: string // '{First} {Last} at {Company} replied to touch {N}.'
slack_hubspot_deep_link: string // 'https://app.hubspot.com/contacts/{portal_id}/contact/{contact_id}'- HubSpot Sequences API enrolment requires Sales Hub Professional or Enterprise. Confirm seat level is in place before this agent is built. If the team is on Starter, the automation platform must manage the send schedule natively using Gmail API direct sends on a timed queue.
- Gmail sending limits: Google Workspace accounts are capped at 2,000 outbound emails per day per sending account. At 120 prospects per month across a 7-touch cadence this equates to a maximum burst of approximately 840 emails per week, well within limits. If volume scales above 400 per day, domain warm-up and a dedicated sending subdomain must be configured before go-live.
- HubSpot Timeline Events API is used to write custom activity log entries for any events not natively captured by HubSpot (e.g. multi-step reply routing). The custom event schema must be registered in the HubSpot portal before build.
- Slack Web API: the bot must be installed to the team's Slack workspace with the 'chat:write' and 'users:read.email' OAuth scopes. The Slack bot token must be stored in the shared credential store under key 'SLACK_BOT_TOKEN'.
- Bounce handling: on an 'EMAIL_BOUNCED' event the agent must unenrol the contact from the sequence, set 'hs_cadence_status: Bounced', create a HubSpot task for the rep to verify the email address, and halt all further touches for that contact.
- The Slack 'slack_user_id' property must be populated for every rep in HubSpot before go-live. If the property is null, the fallback is to post the alert to a shared Slack channel named '#sdr-alerts' (channel ID to be confirmed with the process owner at build kickoff).
03End-to-end data flow
// ============================================================
// OUTBOUND PROSPECTING AUTOMATION: END-TO-END DATA FLOW
// Process: Outbound Prospecting & Cold Outreach Cadences
// Volume: ~120 prospects/month | 3 agents | 1 retained human step
// ============================================================
// --- TRIGGER ---
// Source: HubSpot contact created or updated
// Condition: contact.properties.lifecyclestage == 'subscriber'
// AND contact.properties.hs_lead_status == 'To Contact'
// Alternate: New row appended to Google Sheet import list
// -> automation platform maps row to HubSpot contact via Contacts API (POST /crm/v3/objects/contacts)
INPUT {
contact_id: string // HubSpot internal ID, e.g. '12345678'
contact_email: string // e.g. 'alex.kim@prospectco.com'
contact_first_name:string // e.g. 'Alex'
contact_last_name: string // e.g. 'Kim'
company_domain: string // e.g. 'prospectco.com' (derived from email)
hubspot_owner_id: string // Owning rep's HubSpot user ID
slack_user_id: string // Rep's Slack user ID (from HubSpot custom property)
}
// ============================================================
// AGENT 1: PROSPECT ENRICHMENT AGENT
// Replaces: Step 1 (list build) and Step 2 (manual enrichment)
// ============================================================
// Call 1a: Clearbit Enrichment API
GET https://person.clearbit.com/v2/combined/find?email={contact_email}
-> clearbit_response {
person.title: -> hs_enriched_job_title
company.metrics.employees: -> hs_enriched_company_size
company.tech: -> hs_enriched_tech_stack // array, joined as CSV
company.name: -> company_name
}
// Call 1b: Apollo.io People API (supplementary, fills empty fields only)
POST https://api.apollo.io/v1/people/match
body: { email: contact_email, reveal_personal_emails: false }
-> apollo_response {
person.title: -> hs_enriched_job_title // if still null after Clearbit
organization.size: -> hs_enriched_company_size // if still null after Clearbit
person.employment_history[0].current_job_functions: -> additional context
}
// Call 1c: Apollo.io Organization API (trigger event lookup)
POST https://api.apollo.io/v1/organizations/enrich
body: { domain: company_domain }
-> apollo_org_response {
organization.latest_funding_round_date: -> hs_enriched_trigger_event // if recent
organization.short_description: -> appended to hs_enrichment_raw_payload
}
// Write enriched fields to HubSpot
PATCH https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}
body: {
hs_enriched_job_title: string,
hs_enriched_company_size: string,
hs_enriched_tech_stack: string,
hs_enriched_trigger_event:string,
hs_enrichment_source: 'Clearbit' | 'Apollo.io' | 'Both' | 'Sparse',
hs_enrichment_status: 'Complete' | 'Sparse' | 'Failed',
hs_enrichment_timestamp: datetime // ISO 8601 UTC
}
// If hs_enrichment_status == 'Failed' OR 'Sparse': halt chain, create rep task, exit.
// ============================================================
// AGENT HANDOFF 1 -> 2
// Condition: hs_enrichment_status == 'Complete'
// Mechanism: HubSpot property-change webhook fires to automation platform
// ============================================================
// ============================================================
// AGENT 2: PERSONALISATION DRAFTING AGENT
// Replaces: Step 3 (opening line) and Step 4 (sequence load, partial)
// ============================================================
// Read enriched fields from HubSpot (GET /crm/v3/objects/contacts/{contact_id})
PROMPT_INPUT {
first_name: contact_first_name,
company: company_name,
job_title: hs_enriched_job_title,
company_size: hs_enriched_company_size,
tech_stack: hs_enriched_tech_stack,
trigger_event: hs_enriched_trigger_event
}
// Call OpenAI Chat Completions API
POST https://api.openai.com/v1/chat/completions
body: {
model: 'gpt-4o',
max_tokens: 300,
messages: [
{ role: 'system', content: SYSTEM_PROMPT }, // tone + offer context
{ role: 'user', content: USER_PROMPT } // structured with PROMPT_INPUT fields
]
}
-> openai_response.choices[0].message.content -> hs_personalisation_draft (string, max 30 words)
// Write draft to HubSpot contact + create review task
PATCH /crm/v3/objects/contacts/{contact_id}
body: { hs_personalisation_draft: string, hs_draft_status: 'Pending Review' }
POST https://api.hubapi.com/crm/v3/objects/tasks
body: {
hs_task_subject: 'Review personalisation draft for {first_name} {last_name}',
hs_task_type: 'TODO',
hubspot_owner_id: hubspot_owner_id,
hs_timestamp: today_business_day_end,
associations: [{ to: { id: contact_id }, types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 9 }] }]
}
-> hs_task_id: string (written back to contact property)
// ============================================================
// RETAINED HUMAN STEP: REP REVIEWS AND APPROVES DRAFT IN HUBSPOT
// Rep sets hs_draft_status to 'Approved' or 'Edited and Approved'
// hs_approved_personalisation is written (may differ from draft)
// If no action within 48 business hours: Slack nudge fires to slack_user_id
// ============================================================
// ============================================================
// AGENT HANDOFF 2 -> 3
// Condition: hs_draft_status == 'Approved' OR 'Edited and Approved'
// Mechanism: HubSpot property-change webhook fires to automation platform
// ============================================================
// ============================================================
// AGENT 3: CADENCE EXECUTION AND LOGGING AGENT
// Replaces: Steps 5, 6, 7, 8, 9, 11
// ============================================================
// Enrol contact in HubSpot sequence (injects approved personalisation into touch one token)
POST https://api.hubapi.com/automation/v4/sequences/enrollments
body: {
sequenceId: hs_sequence_id, // pre-configured sequence ID for this cadence
contactId: contact_id,
startDateTime: next_business_morning_09:00_local,
tokenValues: {
personalisation_opener: hs_approved_personalisation
}
}
// For each touch event received via HubSpot webhook (event subscription):
// event_type: 'EMAIL_SENT' | 'EMAIL_OPENED' | 'EMAIL_CLICKED' | 'EMAIL_REPLIED'
// | 'EMAIL_BOUNCED' | 'MEETING_BOOKED'
// Log every event to HubSpot contact timeline
POST https://api.hubapi.com/crm/v3/timeline/events
body: {
eventTemplateId: CUSTOM_EVENT_TEMPLATE_ID, // registered before build
objectId: contact_id,
tokens: {
touch_number: integer,
event_type: string,
event_timestamp:datetime
}
}
// Update contact properties after each event
PATCH /crm/v3/objects/contacts/{contact_id}
body: {
hs_last_touch_number: integer,
hs_last_touch_date: datetime,
hs_cadence_status: 'Active' | 'Replied' | 'Booked' | 'No Response' | 'Bounced',
hs_total_touches_sent:integer
}
// On EMAIL_REPLIED or MEETING_BOOKED: post Slack alert
POST https://slack.com/api/chat.postMessage
body: {
channel: slack_user_id, // DM to owning rep; fallback: '#sdr-alerts' channel ID
text: '{first_name} {last_name} at {company_name} replied to touch {touch_number}. View record: https://app.hubspot.com/contacts/{portal_id}/contact/{contact_id}'
}
// On EMAIL_BOUNCED: unenrol, set hs_cadence_status: 'Bounced', create rep task, halt.
// On cadence completion with no reply (all touches sent, no response):
PATCH /crm/v3/objects/contacts/{contact_id}
body: {
hs_cadence_status: 'No Response',
hs_cadence_outcome: 'No Response',
lifecyclestage: 'other' // or custom 'archived' stage per team configuration
}
// ============================================================
// END OF FLOW
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Outbound Prospecting & Cold Outreach Cadences.