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
Influencer & Partnership Outreach Automation
[YourCompany.com] · Marketing Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to implement the three-agent Influencer and Partnership Outreach automation end to end. It covers the current-state step map with bottlenecks flagged, full agent specifications with IO contracts, the complete data flow from trigger to output, and the recommended build stack with environment and credential requirements. The owner-facing process and SOP documentation is kept separate. Everything in this pack is build-ready: read it alongside the Integration and API Spec and the Test and QA Plan before writing any workflow code.
01Process snapshot
02Agent specifications
Monitors Notion for campaign briefs marked as ready, reads the target criteria (niche, audience size range, channel), generates a qualified prospect shortlist using those parameters, and calls Hunter.io to retrieve and verify a business email address for each prospect. Each verified prospect is written as a new row to a designated Google Sheet with all enrichment fields populated. Rows where Hunter.io cannot return a verified email are flagged in a dedicated status column rather than silently skipped, so the marketing coordinator can action them manually. This agent replaces the two largest research steps in the current-state flow. Estimated build time: 10 hours. Complexity: Moderate.
// Input notion.brief.campaign_name : string notion.brief.target_niche : string notion.brief.audience_size_min : integer notion.brief.audience_size_max : integer notion.brief.channel : string // e.g. Instagram, YouTube, Blog notion.brief.engagement_benchmark : string // optional, e.g. '3% ER minimum' notion.brief.campaign_angle : string // used downstream by Pitch Drafting Agent // Output (one row per prospect written to Google Sheets) sheets.row.prospect_name : string sheets.row.handle : string sheets.row.niche : string sheets.row.estimated_audience_size : integer sheets.row.channel : string sheets.row.email_address : string | null sheets.row.email_verified : boolean sheets.row.email_status : string // 'verified' | 'unverified' | 'not_found' sheets.row.campaign_name : string // copied from brief sheets.row.brief_notion_id : string // Notion page ID for downstream reference sheets.row.research_timestamp : ISO8601
- Notion integration must use an internal integration token scoped to the campaign briefs database only. The token must have read access. Confirm the Notion workspace and database ID before build.
- The Google Sheets output tab must be named 'Prospect Staging' and must not overwrite existing rows from prior campaigns. Append-only writes. Include a campaign_name column to allow filtering per batch.
- Hunter.io must use the Domain Search endpoint first, then the Email Finder endpoint if domain search returns no results. Cap lookups at 200 per campaign run to stay within the Hunter.io Starter plan limit of 500 searches/month. Confirm the active Hunter.io plan tier before build.
- Prospects where email_status is 'not_found' must still be written to the sheet with a status of 'Manual lookup required'. Do not skip or discard these rows.
- Dedupe check: before writing a row, query the sheet for an existing row with the same handle and campaign_name. If found, skip and log a duplicate warning to the error log table.
- The research shortlist generation step uses the automation platform's built-in LLM or a connected AI model. Confirm which model is available in the chosen platform and whether output quality requires a prompt iteration pass before go-live.
- The campaign_angle field from the Notion brief must be passed through to the Google Sheet row for use by the Pitch Drafting Agent downstream. Do not drop this field.
Reads each validated prospect row from Google Sheets where email_verified is true or email_status is 'verified', then generates a personalised outreach pitch email referencing the prospect's content niche and the campaign angle stored in the brief. Each draft is saved to Gmail via the Drafts API under the sending account. In parallel, the agent creates a new HubSpot contact record for the prospect and sets the contact's lifecycle stage and a campaign tag. The Gmail draft ID is written back to the Google Sheet row and stored on the HubSpot contact record so the follow-up agent can reference it later. No email is sent at this stage. The human approval gate at step 6 is mandatory and must not be bypassed under any build configuration. Estimated build time: 10 hours. Complexity: Moderate.
// Input (read from Google Sheets row) sheets.row.prospect_name : string sheets.row.handle : string sheets.row.niche : string sheets.row.estimated_audience_size : integer sheets.row.channel : string sheets.row.email_address : string sheets.row.campaign_name : string sheets.row.brief_notion_id : string sheets.row.campaign_angle : string // fetched from Notion via brief_notion_id // Output gmail.draft.draft_id : string // Gmail draft ID, stored back to sheet gmail.draft.to : string // prospect email_address gmail.draft.subject : string // generated by agent gmail.draft.body_html : string // personalised pitch body hubspot.contact.contact_id : string // newly created HubSpot contact hubspot.contact.email : string hubspot.contact.properties.niche : string hubspot.contact.properties.channel : string hubspot.contact.properties.campaign_tag : string hubspot.contact.properties.draft_status : string // 'draft_created' hubspot.contact.properties.gmail_draft_id : string sheets.row.draft_status : string // written back: 'draft_created' sheets.row.hubspot_contact_id : string // written back // On approval (triggered by marketing manager sending the draft from Gmail) hubspot.contact.properties.draft_status : string // updated to 'sent' hubspot.contact.properties.send_timestamp : ISO8601 sheets.row.draft_status : string // updated to 'sent' sheets.row.send_timestamp : ISO8601
- Gmail OAuth must use the sending account that the marketing manager monitors. Required scopes: gmail.compose, gmail.readonly, gmail.send (send scope used only when the manager triggers dispatch, not by the agent). Confirm the Google Workspace account and that OAuth consent has been granted before build.
- The pitch generation prompt must include: prospect_name, handle, niche, channel, estimated_audience_size, and campaign_angle. The prompt template must be stored as a configurable variable in the automation platform's credential or environment store, not hardcoded, so the marketing team can update brand voice without a code change.
- HubSpot contact creation must check for an existing contact with the same email address before creating a new record. If a duplicate is found, update the existing record with the campaign tag rather than creating a second contact. Use HubSpot's Contact Search API (POST /crm/v3/objects/contacts/search) for the pre-check.
- HubSpot integration requires Sales Hub Starter or higher for sequence enrolment used by the Follow-Up and Pipeline Agent. Confirm the active HubSpot tier before starting this agent's build. Free-tier HubSpot cannot enrol contacts in sequences.
- The draft_status field in Google Sheets acts as the state machine for this agent. Only rows with draft_status equal to 'pending' are processed. After draft creation, the agent sets draft_status to 'draft_created'. This prevents reprocessing on subsequent poll cycles.
- If Gmail draft creation fails for any reason, write draft_status as 'draft_error' to the sheet row and insert a record into the error log table. Do not silently continue.
Monitors HubSpot contacts whose draft_status is 'sent' and checks for reply activity. Non-responders after the initial send are enrolled in a two-email HubSpot follow-up sequence timed at day 4 and day 9. When a reply or link click is detected on a contact, the agent cancels any pending sequence steps, updates the HubSpot deal stage to 'Replied', and posts a Slack alert to the marketing channel with the prospect name, reply summary, and a direct link to the HubSpot contact record. All deal stage transitions are logged with a timestamp property on the HubSpot contact. This agent replaces the four final manual steps in the current-state process. Estimated build time: 8 hours. Complexity: Moderate.
// Input hubspot.contact.contact_id : string hubspot.contact.email : string hubspot.contact.properties.draft_status : string // must equal 'sent' hubspot.contact.properties.send_timestamp : ISO8601 hubspot.contact.properties.sequence_enrolled : boolean gmail.thread.reply_received : boolean // polled via Gmail API gmail.thread.click_detected : boolean // via HubSpot email tracking // Output: enrolment path (no reply detected within 4 days) hubspot.sequence.enrolment_id : string hubspot.contact.properties.sequence_enrolled : boolean // set to true hubspot.contact.properties.sequence_step : string // 'followup_1' | 'followup_2' // Output: reply detected hubspot.contact.properties.deal_stage : string // updated to 'Replied' hubspot.contact.properties.reply_timestamp : ISO8601 hubspot.sequence.cancelled : boolean // sequence unenrolled on reply slack.message.channel : string // '#marketing' or configured channel name slack.message.text : string // includes prospect name, reply summary, HubSpot link // Output: no reply after followup_2 (day 9) hubspot.contact.properties.deal_stage : string // updated to 'No Response' hubspot.contact.properties.sequence_enrolled : boolean // set to false
- HubSpot sequence enrolment requires the Sales Hub Starter plan or higher. The sequence must be pre-built in HubSpot before the agent attempts enrolment. The sequence ID must be stored as an environment variable in the automation platform. Confirm the sequence ID with the marketing team during discovery.
- Reply detection uses the Gmail API threads.list and messages.get endpoints filtered by the sending account's inbox. Poll interval should be no shorter than 30 minutes to avoid hitting Gmail API rate limits (250 quota units per second per user). Consider using HubSpot's built-in email open and reply tracking as a secondary signal where available.
- The Slack alert must use an Incoming Webhook URL configured for the '#marketing' channel (or the channel name confirmed by the marketing team). Store the webhook URL as an environment variable. The message payload must include: prospect first name, niche, a one-line reply excerpt (first 120 characters), and a hyperlink to the HubSpot contact record.
- Sequence cancellation on reply: use HubSpot's POST /crm/v3/objects/contacts/{contactId}/associations to cross-reference the enrolled sequence, then call DELETE on the enrolment. Confirm that the HubSpot account's sequence unenrolment API is available at the active tier.
- The deal stage names 'Replied' and 'No Response' must match the exact stage labels configured in the HubSpot pipeline. Confirm stage names and IDs from the HubSpot pipeline settings before build. Store stage IDs as environment variables.
- If Slack webhook delivery fails, log the failure to the error log table and retry once after 5 minutes. Do not suppress the alert silently.
03End-to-end data flow
// ============================================================
// INFLUENCER & PARTNERSHIP OUTREACH -- END-TO-END DATA FLOW
// ============================================================
// TRIGGER
// Marketing Manager sets Notion brief status to 'Ready'
NOTION.database.query(
filter: { property: 'status', equals: 'Ready' }
)
-> brief.campaign_name : string
-> brief.target_niche : string
-> brief.audience_size_min : integer
-> brief.audience_size_max : integer
-> brief.channel : string
-> brief.campaign_angle : string
-> brief.notion_page_id : string
// ============================================================
// AGENT 1 HANDOFF: Prospect Research Agent
// ============================================================
// Step 1: Generate prospect shortlist from brief criteria
RESEARCH_AGENT.generate_shortlist(
niche : brief.target_niche,
audience_min : brief.audience_size_min,
audience_max : brief.audience_size_max,
channel : brief.channel
)
-> prospects[] // array of candidate objects
.prospect_name : string
.handle : string
.niche : string
.estimated_audience_size : integer
.channel : string
// Step 2: Email enrichment via Hunter.io for each prospect
FOR EACH prospect IN prospects[]:
HUNTER_IO.email_finder(
full_name : prospect.prospect_name,
domain : derived from prospect.handle or known brand domain
)
-> prospect.email_address : string | null
-> prospect.email_verified : boolean
-> prospect.email_status : 'verified' | 'unverified' | 'not_found'
// Step 3: Write enriched rows to Google Sheets (append-only)
GOOGLE_SHEETS.append_row(
tab : 'Prospect Staging',
row_fields : {
prospect_name : prospect.prospect_name,
handle : prospect.handle,
niche : prospect.niche,
estimated_audience_size : prospect.estimated_audience_size,
channel : prospect.channel,
email_address : prospect.email_address,
email_verified : prospect.email_verified,
email_status : prospect.email_status,
campaign_name : brief.campaign_name,
brief_notion_id : brief.notion_page_id,
campaign_angle : brief.campaign_angle,
draft_status : 'pending',
research_timestamp : now()
}
)
// ============================================================
// AGENT 2 HANDOFF: Pitch Drafting Agent
// ============================================================
// Step 4: Read verified rows from Prospect Staging where draft_status = 'pending'
GOOGLE_SHEETS.read_rows(
tab : 'Prospect Staging',
filter : { email_status: 'verified', draft_status: 'pending' }
)
// Step 5: Generate personalised pitch per prospect
FOR EACH row IN verified_rows:
PITCH_AGENT.generate_draft(
prospect_name : row.prospect_name,
handle : row.handle,
niche : row.niche,
channel : row.channel,
estimated_audience_size : row.estimated_audience_size,
campaign_angle : row.campaign_angle
)
-> draft.subject : string
-> draft.body_html : string
// Step 6: Save draft to Gmail
GMAIL.drafts.create(
to : row.email_address,
subject : draft.subject,
body : draft.body_html
)
-> gmail.draft_id : string
// Step 7: Create HubSpot contact (with dedupe check)
HUBSPOT.contacts.search( email: row.email_address )
IF no_existing_contact:
HUBSPOT.contacts.create(
email : row.email_address,
firstname : row.prospect_name,
niche : row.niche,
channel : row.channel,
campaign_tag : row.campaign_name,
draft_status : 'draft_created',
gmail_draft_id : gmail.draft_id
)
-> hubspot.contact_id : string
ELSE:
HUBSPOT.contacts.update( existing_contact_id, campaign_tag, gmail_draft_id )
-> hubspot.contact_id : string
// Step 8: Write back to Google Sheets row
GOOGLE_SHEETS.update_row(
draft_status : 'draft_created',
hubspot_contact_id : hubspot.contact_id,
gmail_draft_id : gmail.draft_id
)
// ============================================================
// HUMAN GATE: Marketing Manager reviews and sends Gmail drafts
// No automation action occurs until manager sends the draft.
// On send, HubSpot contact draft_status is updated to 'sent'
// via a HubSpot workflow or a polling check on Gmail sent folder.
// ============================================================
GMAIL.sent.poll( draft_id: gmail.draft_id )
IF draft_sent:
HUBSPOT.contacts.update(
contact_id : hubspot.contact_id,
draft_status : 'sent',
send_timestamp : now()
)
GOOGLE_SHEETS.update_row(
draft_status : 'sent',
send_timestamp : now()
)
// ============================================================
// AGENT 3 HANDOFF: Follow-Up and Pipeline Agent
// ============================================================
// Step 9: Poll HubSpot for sent contacts not yet enrolled in sequence
HUBSPOT.contacts.search(
filter: { draft_status: 'sent', sequence_enrolled: false }
)
// Step 10: Enrol in follow-up sequence (days 4 and 9)
FOR EACH contact IN sent_contacts:
HUBSPOT.sequences.enrol(
contact_id : contact.contact_id,
sequence_id : ENV.HUBSPOT_SEQUENCE_ID
)
HUBSPOT.contacts.update(
sequence_enrolled : true,
sequence_step : 'followup_1'
)
// Step 11: Monitor for reply or click
GMAIL.threads.poll( contact_email: contact.email )
IF reply_detected OR click_detected:
HUBSPOT.sequences.unenrol( contact_id: contact.contact_id )
HUBSPOT.contacts.update(
deal_stage : ENV.HUBSPOT_STAGE_ID_REPLIED,
reply_timestamp : now(),
sequence_enrolled : false
)
SLACK.incoming_webhook.post(
channel : ENV.SLACK_CHANNEL,
text : 'Hot lead: {prospect_name} ({niche}) replied. View: {hubspot_contact_url}'
)
// Step 12: No reply after followup_2 (day 9 elapsed)
IF no_reply AND sequence_completed:
HUBSPOT.contacts.update(
deal_stage : ENV.HUBSPOT_STAGE_ID_NO_RESPONSE,
sequence_enrolled : false
)
// ============================================================
// END OF FLOW
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Influencer & Partnership Outreach.