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
Customer Segmentation & List Management
[YourCompany.com] · Marketing Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to configure, wire, and validate the Customer Segmentation and List Management automation from scratch. It covers the current manual process and its bottlenecks, the specification for each of the three agents, the end-to-end data flow with exact field names, and the recommended build stack including estimated total effort. You will not find business-case justification here; that lives in the ROI and Business Case document. What follows is the technical ground truth for the build.
01Process snapshot
02Agent specifications
This agent is the entry point for every sync cycle. It listens for contact create or update events from HubSpot via webhook and for new Typeform submissions via webhook, then fetches the full contact record from HubSpot using the contact ID from the event payload. It normalises field names across both sources (HubSpot properties and Typeform response fields map to a shared schema before any further processing). Once both datasets are assembled, it routes the combined set through Segment's identify and merge APIs to resolve duplicate records by email address, emitting a single canonical record per unique contact. The clean dataset is then passed downstream to the Segmentation Rules Agent. Nothing is written to HubSpot or Mailchimp at this stage.
// Input
HubSpot webhook payload: { contactId, changeSource, propertyName, timestamp }
Typeform webhook payload: { form_id, token, submitted_at, answers: [ { field: { id, type }, email, text } ] }
// Normalised schema (internal, passed to Segment identify call)
{
email: string, // primary dedup key
first_name: string,
last_name: string,
hs_contact_id: string, // HubSpot internal ID
tf_response_id: string, // Typeform token, null if source is HubSpot
source: 'hubspot' | 'typeform',
last_modified: ISO8601,
lifecycle_stage: string,
purchase_count: integer,
engagement_score: float,
subscription_status: string
}
// Output (passed to Segmentation Rules Agent)
{
dedup_run_id: uuid,
records_in: integer,
records_out: integer, // after merge; duplicates resolved
merged_pairs: [ { canonical_email, merged_from_ids: [string] } ],
contacts: [ <normalised schema above> ]
}- HubSpot tier must be at least Professional to access the Webhooks API and the full Contacts v3 property set. Confirm API key scope includes crm.objects.contacts.read and crm.objects.contacts.write before build begins.
- Typeform webhook must be registered on the specific form ID used for lead capture. The form ID must be provided by the marketing team. Do not assume a single form; confirm whether multiple forms are in scope.
- Segment dedup strategy: match on email address (lowercase, trimmed) as the primary key. If two records share an email but have different hs_contact_id values, retain the record with the more recent last_modified and log the merged ID pair. Do not delete HubSpot records via API at this stage; flag only.
- Fallback behaviour: if the Typeform webhook payload contains no email field answer, the record is written to the exception log in Google Sheets with reason 'missing_email' and processing halts for that record only. The rest of the batch continues.
- Segment must be on a plan that includes the Profile API (Business tier or above). Confirm with the client that the $120/month Segment subscription covers this endpoint before configuring the merge call.
- The daily scheduled run should execute at 02:00 UTC to avoid overlap with campaign send windows. Confirm the preferred timezone with the marketing manager.
- Idempotency: the dedup agent must store processed webhook event IDs (using the HubSpot changeSource timestamp + contactId composite key) to prevent double-processing on retry. Use the automation platform's built-in key-value store or a Supabase row for this.
This agent receives the clean deduplicated contact dataset from the Data Merge and Dedup Agent and evaluates every contact against the configured segment rules. Rules are stored as a structured configuration object (not hard-coded) so the marketing manager can update criteria without a code change. The agent first pulls the current suppression list from Mailchimp (unsubscribes and bounces) and excludes any matching emails from all segment assignments before syncing. For contacts that cannot be confidently assigned to a segment (for example, a contact whose purchase history is present but engagement score is null), the agent sets a flag and routes the record to the exception list rather than assigning a default segment. All remaining contacts are synced to Mailchimp audience memberships via the Mailchimp Batch API and their HubSpot contact properties are updated to reflect the new segment tags and suppression flags.
// Input
{
dedup_run_id: uuid,
contacts: [ <normalised contact schema from Agent 1> ],
segment_rules: [
{ segment_id: string, label: string, criteria: { field: string, operator: string, value: any } }
]
}
// Suppression fetch (internal, before assignment)
GET /3.0/lists/{list_id}/members?status=unsubscribed,bounced
-> suppressed_emails: Set<string>
// Segment assignment output (per contact)
{
email: string,
hs_contact_id: string,
assigned_segment: string | null, // null if flagged for review
suppressed: boolean,
flag_reason: string | null, // e.g. 'null_engagement_score'
mailchimp_audience_id: string | null,
mailchimp_tag: string | null
}
// Output (passed to Reporting and Notification Agent)
{
run_id: uuid,
segment_counts: { [segment_label]: integer },
records_synced: integer,
records_suppressed: integer,
records_flagged: integer,
flagged_contacts: [ { email, hs_contact_id, flag_reason } ],
hubspot_update_errors: [ { hs_contact_id, error_message } ],
mailchimp_batch_id: string
}
// On approval (flagged contact reviewed by marketing manager)
Marketing manager assigns segment via Slack action or Google Sheet update.
Re-trigger: Segmentation Rules Agent re-evaluates the specific contact with override.
Override payload: { email, hs_contact_id, override_segment: string, reviewed_by: string, reviewed_at: ISO8601 }- Segment rules must be fully documented and signed off by the marketing manager before this agent is configured. The build cannot proceed with vague or verbal-only criteria. The rule configuration object must be stored in the automation platform's credential or config store, not in workflow logic directly.
- Mailchimp Batch API must be used for syncs involving more than 500 contacts in a single run to avoid hitting the standard API rate limit of 10 requests per second. Implement exponential backoff on 429 responses with a maximum of 3 retries before marking the batch as failed.
- Mailchimp audience ID must be provided for each target segment. Do not create new audiences dynamically; update existing ones only. Confirm all audience IDs with the marketing team before build.
- HubSpot PATCH calls must update the following properties per contact: hs_lead_status, hs_email_optout, and a custom property 'fs_segment_tag' (create this property in HubSpot before build if it does not already exist). Confirm the custom property namespace with the HubSpot account owner.
- Contacts in the suppression set must have their HubSpot 'hs_email_optout' property set to true in the same PATCH call. This write must happen even if the Mailchimp sync fails for that record.
- Ambiguity threshold: a contact is flagged for review if any required segmentation field is null or if the contact matches more than one segment's top-level criteria simultaneously. Do not default-assign to a catch-all segment without explicit instruction from the marketing manager.
- Segment Track API should emit a 'segment_assigned' event per contact for audit purposes, including run_id, email hash (SHA-256, not plain text), assigned_segment, and timestamp.
This agent fires after the Segmentation Rules Agent completes its sync run and receives the run summary payload. It formats a structured Slack message showing segment counts, records synced, records suppressed, and a count of contacts flagged for human review. If any contacts are flagged, the Slack message includes a direct link to the flagged contacts tab in the master Google Sheet. The agent also appends a timestamped row to the master Google Sheet log covering the same metrics. If the Segmentation Rules Agent reported any HubSpot update errors, the Reporting Agent raises an amber alert in Slack rather than a standard green summary, making failures visible without requiring the team to check logs manually.
// Input
{
run_id: uuid,
run_timestamp: ISO8601,
segment_counts: { [segment_label]: integer },
records_synced: integer,
records_suppressed: integer,
records_flagged: integer,
hubspot_update_errors: [ { hs_contact_id, error_message } ],
mailchimp_batch_id: string
}
// Output: Slack message payload
{
channel: '#marketing-ops', // confirm channel name with team
blocks: [
{ type: 'header', text: 'Segmentation run complete — <run_timestamp>' },
{ type: 'section', fields: [ segment_counts mapped as label/value pairs ] },
{ type: 'section', text: 'Synced: <records_synced> | Suppressed: <records_suppressed> | Flagged for review: <records_flagged>' },
// If records_flagged > 0:
{ type: 'section', text: 'View flagged contacts: <google_sheets_tab_url>' },
// If hubspot_update_errors.length > 0:
{ type: 'section', text: ':warning: <n> HubSpot update errors. Check error log tab.' }
]
}
// Output: Google Sheets append row
[ run_id, run_timestamp, records_synced, records_suppressed, records_flagged,
segment_counts_json_string, mailchimp_batch_id, hubspot_error_count ]- Slack channel name must be confirmed with the marketing team before build. The bot token must have chat:write scope and must be invited to the channel. Do not use an Incoming Webhook URL if the team anticipates needing interactive Slack actions for exception approval in a future phase.
- Google Sheets log tab must exist before the agent runs. The spreadsheet ID and sheet tab name (suggest 'Run Log') must be stored in the credential store. The service account used for Sheets API access requires Editor permission on the specific spreadsheet.
- If the Segmentation Rules Agent passes a hubspot_update_errors array with one or more entries, the Slack message tone changes to amber (use a yellow circle emoji prefix on the header) to distinguish it from a clean run. This is a visual-only signal; no automated retry is triggered by this agent.
- Google Sheets row append must be idempotent: check whether a row with the same run_id already exists in the log tab before appending, to prevent duplicate rows on workflow retry.
- This agent is Simple complexity and should be built and tested first as a smoke test for Slack and Sheets connectivity before the heavier agents are wired up.
03End-to-end data flow
// ─────────────────────────────────────────────────────────────────
// TRIGGER LAYER
// ─────────────────────────────────────────────────────────────────
// Event A: HubSpot contact created or property changed
HubSpot Webhook ->
payload.contactId : string // e.g. '12345678'
payload.changeSource : string // e.g. 'CRM_UI' | 'API' | 'FORM'
payload.propertyName : string // e.g. 'email' | 'lifecyclestage'
payload.timestamp : integer // Unix ms
// Event B: Typeform form submitted
Typeform Webhook ->
payload.form_id : string
payload.token : string // unique response token
payload.submitted_at : ISO8601
payload.answers[] : array // field.type includes 'email', 'text', 'number'
// Event C: Daily scheduled run (02:00 UTC)
Scheduler ->
{ source: 'schedule', run_date: ISO8601 }
// ─────────────────────────────────────────────────────────────────
// AGENT 1: Data Merge and Dedup Agent
// ─────────────────────────────────────────────────────────────────
// Step 1.1: Fetch full HubSpot contact record
GET /crm/v3/objects/contacts/{contactId}
?properties=email,firstname,lastname,lifecyclestage,hs_lead_status,
num_associated_deals,hs_email_optout,last_modified_date,
hubspot_score,hs_analytics_num_visits
-> hs_contact: { id, properties: { email, firstname, lastname, ... } }
// Step 1.2: Parse Typeform response into normalised schema
typeform.answers.find(a => a.field.type === 'email').email
-> tf_email: string
typeform.answers.find(a => a.field.ref === 'first_name').text
-> tf_first_name: string
// ... additional Typeform field mappings per confirmed form structure
// Step 1.3: Normalise both sources to shared contact schema
contact_normalised = {
email : lowercase(trim(source.email)),
first_name : string,
last_name : string,
hs_contact_id : string | null,
tf_response_id : string | null,
source : 'hubspot' | 'typeform',
last_modified : ISO8601,
lifecycle_stage : string,
purchase_count : integer,
engagement_score : float | null,
subscription_status: string
}
// Step 1.4: Dedup via Segment Profile API
POST /v1/identify
{ userId: email, traits: contact_normalised }
GET /v1/profiles/email:{email}/traits
-> canonical_profile: { userId, traits, merged_ids: [string] }
// Duplicate resolution: retain record with max(last_modified)
// Log merged pairs: { canonical_email, merged_from_ids }
// HANDOFF A -> B
dedup_output = {
dedup_run_id : uuid,
records_in : integer,
records_out : integer,
merged_pairs : [ { canonical_email, merged_from_ids } ],
contacts : [ contact_normalised ] // deduplicated set
}
// ─────────────────────────────────────────────────────────────────
// AGENT 2: Segmentation Rules Agent
// ─────────────────────────────────────────────────────────────────
// Step 2.1: Fetch Mailchimp suppression list
GET /3.0/lists/{audience_id}/members?status=unsubscribed&count=1000
GET /3.0/lists/{audience_id}/members?status=bounced&count=1000
-> suppressed_emails: Set<string> // lowercase email addresses
// Step 2.2: Evaluate segment rules per contact
for each contact in dedup_output.contacts:
if contact.email in suppressed_emails:
contact.suppressed = true
contact.assigned_segment = null
continue
match_results = segment_rules.filter(rule => evaluate(contact, rule.criteria))
if match_results.length === 1:
contact.assigned_segment = match_results[0].segment_id
elif match_results.length === 0 OR contact.engagement_score === null:
contact.assigned_segment = null
contact.flag_reason = 'no_rule_match' | 'null_engagement_score'
elif match_results.length > 1:
contact.assigned_segment = null
contact.flag_reason = 'multi_rule_conflict'
// Step 2.3: Sync assigned contacts to Mailchimp via Batch API
POST /3.0/batches
body.operations[] = {
method: 'PUT',
path: '/lists/{audience_id}/members/{md5(email)}',
body: { email_address, status_if_new: 'subscribed',
tags: [assigned_segment], merge_fields: { FNAME, LNAME } }
}
-> mailchimp_batch_id: string
// Step 2.4: Update HubSpot contact properties
PATCH /crm/v3/objects/contacts/{hs_contact_id}
body.properties = {
hs_lead_status : assigned_segment | 'FLAGGED_FOR_REVIEW',
hs_email_optout : contact.suppressed,
fs_segment_tag : assigned_segment | null
}
// Step 2.5: Emit audit event via Segment Track
POST /v1/track
{ userId: sha256(email), event: 'segment_assigned',
properties: { run_id, assigned_segment, suppressed, flag_reason, timestamp } }
// HANDOFF B -> C
seg_output = {
run_id : uuid,
segment_counts : { [segment_label]: integer },
records_synced : integer,
records_suppressed : integer,
records_flagged : integer,
flagged_contacts : [ { email, hs_contact_id, flag_reason } ],
hubspot_update_errors : [ { hs_contact_id, error_message } ],
mailchimp_batch_id : string
}
// ─────────────────────────────────────────────────────────────────
// AGENT 3: Reporting and Notification Agent
// ─────────────────────────────────────────────────────────────────
// Step 3.1: Format and post Slack message
POST https://slack.com/api/chat.postMessage
{ channel: '#marketing-ops',
blocks: [ header, segment_count_fields, summary_line,
flagged_link_if_any, error_warning_if_any ] }
// Step 3.2: Append row to Google Sheets run log
POST /v4/spreadsheets/{spreadsheet_id}/values/{log_tab}!A:H:append
body.values = [[
run_id, run_timestamp, records_synced, records_suppressed,
records_flagged, JSON.stringify(segment_counts),
mailchimp_batch_id, hubspot_update_errors.length
]]
// ─────────────────────────────────────────────────────────────────
// EXCEPTION PATH: Flagged contact reviewed by marketing manager
// ─────────────────────────────────────────────────────────────────
// Marketing manager updates flagged_contacts tab in Google Sheets
// OR responds via Slack interactive action (future phase)
override_payload = {
email : string,
hs_contact_id : string,
override_segment : string,
reviewed_by : string,
reviewed_at : ISO8601
}
// Re-triggers Agent 2 for that contact only, skipping rule evaluation
// and applying override_segment directly to Mailchimp and HubSpot04Recommended build stack
More documents for this process
Every document generated for Customer Segmentation & List Management.