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
VIP Customer Management
[YourCompany.com] · Customer Support Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to implement, wire, and validate the three-agent VIP Customer Management automation. It covers the current-state step map with bottleneck flags, full agent specifications with IO contracts, the end-to-end data flow trace, and the recommended build stack. The process owner's responsibilities are limited to confirming tier thresholds, approving email templates, and providing API credentials before build begins. All construction, testing, and integration work is handled by the FullSpec team.
01Process snapshot
02Agent specifications
Monitors Xero transaction data and HubSpot contact records to determine whether each customer qualifies for VIP status, which tier they belong to, and whether their tier has changed since the last review. On each run the agent pulls cumulative spend figures from Xero, maps them to HubSpot contacts via a shared identifier (email address as primary key, company name as fallback), evaluates the result against the configured tier thresholds, and writes the outcome back to the HubSpot contact record. If the tier has changed, it sets a boolean flag that triggers the VIP Outreach Agent downstream. Complexity: Moderate. Estimated build time: 10 hours.
// Input xero_invoice.contact.email_address // primary join key xero_invoice.contact.name // fallback join key xero_invoice.sub_total // transaction value (USD) xero_invoice.date_string // ISO-8601 date of transaction hubspot_contact.vip_tier // current tier label stored in HubSpot hubspot_contact.vip_qualification_date // date of last tier assignment hubspot_contact.cumulative_spend_usd // running total property in HubSpot // Output hubspot_contact.vip_tier // updated: 'None' | 'Silver' | 'Gold' | 'Platinum' hubspot_contact.vip_qualification_date // updated to today's date if tier changes hubspot_contact.cumulative_spend_usd // updated running total hubspot_contact.vip_tier_changed // boolean: true if tier differs from prior value hubspot_contact.tier_change_direction // 'upgrade' | 'downgrade' | 'no_change'
- Xero API: requires accounting.transactions.read scope on a connected app with OAuth 2.0. Use the Xero Accounting API v2 endpoint GET /api.xro/2.0/Invoices?ContactIDs={id}&Status=PAID. Confirm the organisation's Xero tenant ID is stored in the credential store before build.
- HubSpot API: requires contacts read/write scope (crm.objects.contacts.read, crm.objects.contacts.write). Custom contact properties vip_tier, vip_qualification_date, cumulative_spend_usd, vip_tier_changed, and tier_change_direction must be created in HubSpot before the agent runs. Confirm these are provisioned with the correct data types (enumeration, date, number, bool, enumeration).
- Customer identifier matching: email address is the primary join key between Xero and HubSpot. Where the Xero contact email is absent or does not match any HubSpot contact, the agent must fall back to fuzzy company name matching (Levenshtein distance threshold 0.85). Any contact that cannot be matched after both attempts must be written to the error log table with status 'unmatched_contact' and skipped rather than errored.
- Tier thresholds must be externalised as configuration variables (not hardcoded). The account team must confirm exact dollar values for Silver, Gold, and Platinum before build commences.
- Dedupe: the scheduled Monday run must check vip_tier_changed and reset it to false before processing, to avoid double-triggering the Outreach Agent from both the webhook and the cron on the same contact.
- Downgrade logic: customers whose cumulative spend has not been recalculated in over 365 days should be flagged for manual review rather than auto-downgraded. This behaviour must be confirmed with the process owner before implementation.
Generates and sends personalised outreach emails for newly tiered or tier-changed VIP customers, enrolls them in the correct HubSpot check-in sequence, dispatches quarterly satisfaction surveys via Typeform, and logs all activity on the HubSpot contact timeline. The agent fires immediately on a tier-change event from the VIP Tier Agent and on a scheduled basis to manage ongoing sequence timing. All email content is drawn from pre-approved brand templates parameterised with the customer's first name, tier label, and relevant benefits copy. Complexity: Moderate. Estimated build time: 12 hours.
// Input hubspot_contact.email // recipient address hubspot_contact.firstname // personalisation token hubspot_contact.vip_tier // 'Silver' | 'Gold' | 'Platinum' hubspot_contact.tier_change_direction // 'upgrade' | 'downgrade' hubspot_contact.vip_qualification_date // used to determine sequence day position hubspot_contact.cumulative_spend_usd // optional personalisation in body copy hubspot_contact.hubspot_owner_id // used to set sequence sender identity // Output gmail.sent_message_id // logged to HubSpot timeline hubspot_timeline.event_type // 'vip_welcome_email' | 'checkin_email' | 'survey_sent' hubspot_sequence.enrollment_id // active sequence enrolment record typeform_survey.response_id // survey link dispatched, recorded on contact hubspot_contact.last_outreach_date // updated after each send // On approval (if human-review step is enabled for a specific tier or message type) approval_queue.record_id // staged email held in queue table pending owner review approval_queue.approved_at // timestamp set when owner approves approval_queue.approved_by // owner identifier
- Gmail API: requires gmail.send and gmail.compose OAuth scopes. The sending identity must match the HubSpot contact owner's Google Workspace account. The hubspot_owner_id must resolve to a Gmail address stored in the credential mapping table before the agent can send on behalf of the correct account manager.
- HubSpot Sequences: the agent uses the Sequences API (POST /crm/v3/objects/enrollments) to enrol contacts. Confirm which sequence IDs correspond to Silver, Gold, and Platinum tiers. Sequence IDs must be stored as named configuration variables, not hardcoded.
- Typeform: a unique survey link per contact is generated using the Typeform Hidden Fields feature so responses are automatically attributed to the correct HubSpot contact by email. The Typeform form ID for the VIP satisfaction survey must be confirmed before build.
- Template selection logic: the agent selects the email template based on tier label and tier_change_direction. A mapping table of (tier, direction) to template ID must be created and confirmed with the account team before build. At minimum, templates are needed for: Silver upgrade, Gold upgrade, Platinum upgrade, and the quarterly survey dispatch.
- Fallback behaviour: if the Gmail send fails (e.g. rate limit or auth error), the agent must not re-attempt immediately. It should write the failed send to the error log with status 'email_send_failed' and retry once after 30 minutes. If the retry also fails, a Slack alert must be sent to the support channel.
- Timeline logging: every outreach action must create a HubSpot timeline event using the Custom Timeline Events API so contact history is complete without any manual logging by the account manager.
Watches the Zendesk ticket queue for new submissions from contacts flagged as VIP in HubSpot, applies the correct priority tag to the ticket immediately, and sends a Slack alert to the assigned account manager with customer context and a direct ticket link. Additionally, monitors incoming Typeform survey responses for scores below the configured alert threshold and posts a separate Slack escalation for low-score responses. This agent is fully event-driven and does not run on a schedule. Complexity: Simple. Estimated build time: 8 hours.
// Input (ticket path) zendesk_ticket.id // ticket identifier zendesk_ticket.requester.email // used to look up HubSpot contact zendesk_ticket.subject // included in Slack alert zendesk_ticket.created_at // ISO-8601 timestamp hubspot_contact.vip_tier // queried by email; determines if alert fires hubspot_contact.hubspot_owner_id // resolves to Slack user ID for direct mention // Input (survey path) typeform_response.form_id // validated against configured VIP survey form ID typeform_response.answers // parsed for NPS or satisfaction score field typeform_response.hidden.email // links response back to HubSpot contact typeform_response.submitted_at // ISO-8601 timestamp // Output (ticket path) zendesk_ticket.tags // appended: 'vip_priority', tier label (e.g. 'vip_gold') zendesk_ticket.priority // set to 'urgent' for Platinum, 'high' for Gold and Silver slack_message.channel // direct message to hubspot_owner slack_user_id slack_message.text // customer name, tier, ticket subject, Zendesk URL // Output (survey path) slack_message.channel // designated escalation Slack channel slack_message.text // customer name, score, survey link, HubSpot contact URL hubspot_contact.last_survey_score // written back to HubSpot contact property
- Zendesk API: requires tickets:write scope. Use the Zendesk Tickets API v2 (PUT /api/v2/tickets/{id}.json) to update tags and priority. Confirm that the tags 'vip_priority', 'vip_silver', 'vip_gold', 'vip_platinum' do not conflict with any existing Zendesk tag taxonomy before build.
- Slack API: requires chat:write and users:read OAuth scopes. The owner-to-Slack-user mapping table must be populated before go-live. HubSpot owner IDs must resolve to Slack user IDs via email address lookup using the Slack users.lookupByEmail method.
- Survey alert threshold: the score field and threshold value (e.g. NPS below 7, or satisfaction score below 3 out of 5) must be confirmed with the Support Manager before build. Store the threshold as a named configuration variable.
- VIP check: the agent must query HubSpot by the requester's email address before deciding whether to escalate. If no matching HubSpot contact is found, or if the contact's vip_tier is 'None' or null, the ticket must be left unmodified and the agent exits silently.
- Idempotency: the agent must check whether the tags 'vip_priority' and the tier tag are already present on the ticket before applying them, to prevent duplicate tag entries on re-processed webhook events.
- Error handling: if the Zendesk tag update fails, write to the error log and send a fallback Slack message to the support channel (not the account manager) so a human can apply the tag manually.
03End-to-end data flow
// =========================================================
// VIP CUSTOMER MANAGEMENT: END-TO-END DATA FLOW
// =========================================================
// TRIGGER PATH A: Xero invoice payment webhook
XERO_WEBHOOK.invoice.status == 'PAID'
-> xero_invoice.contact.email_address // primary join key extracted
-> xero_invoice.contact.name // fallback join key extracted
-> xero_invoice.sub_total // transaction amount (USD)
-> xero_invoice.date_string // ISO-8601 payment date
// TRIGGER PATH B: Scheduled cron (Monday 06:00 UTC)
SCHEDULER.weekly_review
-> hubspot.contacts.list(filter: vip_tier != 'None' OR cumulative_spend_usd > 0)
-> iterate each contact as hubspot_contact{}
// =========================================================
// AGENT 1: VIP Tier Agent
// =========================================================
// Step 1: Resolve Xero contact to HubSpot contact
HUBSPOT.contacts.search(email = xero_invoice.contact.email_address)
-> IF no match: fuzzy_match(xero_invoice.contact.name, threshold=0.85)
-> IF still no match: error_log.insert(status='unmatched_contact', raw=xero_invoice{})
-> EXIT this record
// Step 2: Read current HubSpot contact state
hubspot_contact.vip_tier // prior tier label
hubspot_contact.cumulative_spend_usd // prior running total
hubspot_contact.vip_qualification_date
// Step 3: Recalculate cumulative spend
new_cumulative = hubspot_contact.cumulative_spend_usd + xero_invoice.sub_total
// Step 4: Evaluate tier thresholds (configuration variables)
IF new_cumulative >= THRESHOLD_PLATINUM -> new_tier = 'Platinum'
ELSE IF new_cumulative >= THRESHOLD_GOLD -> new_tier = 'Gold'
ELSE IF new_cumulative >= THRESHOLD_SILVER -> new_tier = 'Silver'
ELSE -> new_tier = 'None'
// Step 5: Write outcome to HubSpot contact
HUBSPOT.contacts.update(id = hubspot_contact.id, {
vip_tier: new_tier,
cumulative_spend_usd: new_cumulative,
vip_qualification_date: TODAY() IF new_tier != hubspot_contact.vip_tier,
vip_tier_changed: (new_tier != hubspot_contact.vip_tier),
tier_change_direction: 'upgrade' | 'downgrade' | 'no_change'
})
// =========================================================
// AGENT 1 -> AGENT 2 HANDOFF
// Condition: hubspot_contact.vip_tier_changed == true
// =========================================================
// TRIGGER: HubSpot property-change webhook fires on vip_tier_changed = true
HUBSPOT_WEBHOOK.contact.property_changed(property='vip_tier_changed', value=true)
-> hubspot_contact.id
-> hubspot_contact.email
-> hubspot_contact.firstname
-> hubspot_contact.vip_tier // new tier
-> hubspot_contact.tier_change_direction
-> hubspot_contact.hubspot_owner_id
// =========================================================
// AGENT 2: VIP Outreach Agent
// =========================================================
// Step 1: Select email template
template_id = TEMPLATE_MAP.lookup(vip_tier, tier_change_direction)
-> template.subject, template.body_html // parameterised with firstname, vip_tier
// Step 2: Send tier-up or welcome email via Gmail
GMAIL.messages.send({
to: hubspot_contact.email,
from: OWNER_EMAIL_MAP.lookup(hubspot_contact.hubspot_owner_id),
subject: template.subject,
body: render(template.body_html, {firstname, vip_tier, cumulative_spend_usd})
})
-> gmail.sent_message_id
// Step 3: Log send event on HubSpot timeline
HUBSPOT.timeline.create({
contact_id: hubspot_contact.id,
event_type: 'vip_welcome_email',
timestamp: NOW(),
properties: { gmail_message_id, template_id }
})
// Step 4: Enrol in HubSpot check-in sequence
HUBSPOT.sequences.enroll({
contact_id: hubspot_contact.id,
sequence_id: SEQUENCE_MAP.lookup(vip_tier),
sender_id: hubspot_contact.hubspot_owner_id
})
-> hubspot_sequence.enrollment_id
// Step 5: At 90-day mark, dispatch Typeform survey
IF days_since(hubspot_contact.vip_qualification_date) == 90:
TYPEFORM.link.generate({
form_id: CONFIG.vip_survey_form_id,
hidden_fields: { email: hubspot_contact.email, contact_id: hubspot_contact.id }
})
-> typeform_survey.unique_link
GMAIL.messages.send({ to: hubspot_contact.email, body: render(SURVEY_TEMPLATE, {unique_link}) })
HUBSPOT.timeline.create({ event_type: 'survey_sent', properties: { unique_link } })
HUBSPOT.contacts.update({ last_outreach_date: TODAY() })
// =========================================================
// TRIGGER PATH C: Zendesk ticket created webhook
// TRIGGER PATH D: Typeform survey response webhook
// =========================================================
// =========================================================
// AGENT 3: VIP Support Escalation Agent (ticket path)
// =========================================================
ZENDESK_WEBHOOK.ticket.created
-> zendesk_ticket.id
-> zendesk_ticket.requester.email
-> zendesk_ticket.subject
// Step 1: Look up HubSpot contact by email
HUBSPOT.contacts.search(email = zendesk_ticket.requester.email)
-> IF no match OR vip_tier == 'None': EXIT silently
// Step 2: Apply VIP priority tag in Zendesk
IF 'vip_priority' NOT IN zendesk_ticket.tags:
ZENDESK.tickets.update(id = zendesk_ticket.id, {
tags: APPEND('vip_priority', 'vip_' + vip_tier.lower()),
priority: 'urgent' IF vip_tier == 'Platinum' ELSE 'high'
})
// Step 3: Send Slack alert to account manager
slack_user_id = SLACK.users.lookupByEmail(OWNER_EMAIL_MAP.lookup(hubspot_owner_id))
SLACK.chat.postMessage({
channel: slack_user_id,
text: 'VIP ticket from {firstname} ({vip_tier}): {subject} | {zendesk_ticket_url}'
})
// =========================================================
// AGENT 3: VIP Support Escalation Agent (survey path)
// =========================================================
TYPEFORM_WEBHOOK.form_response.completed
-> typeform_response.hidden.email
-> typeform_response.answers[SCORE_FIELD_REF] // satisfaction or NPS score
-> typeform_response.submitted_at
IF typeform_response.answers[SCORE_FIELD_REF] < CONFIG.survey_alert_threshold:
HUBSPOT.contacts.update({ last_survey_score: score })
SLACK.chat.postMessage({
channel: CONFIG.escalation_channel_id,
text: 'Low survey score ({score}) from {firstname} ({vip_tier}) | {hubspot_contact_url}'
})
// =========================================================
// HUMAN TOUCHPOINT (deliberate, exception only)
// Account Manager reviews escalated ticket or low-score alert
// and responds directly. No further automation fires.
// =========================================================04Recommended build stack
More documents for this process
Every document generated for VIP Customer Management.