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
Demo and Discovery Scheduling
[YourCompany.com] · Sales Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to construct, wire, and validate the Demo and Discovery Scheduling 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 role is limited to approving the pre-call brief template and confirming routing thresholds before build begins. FullSpec handles all orchestration, integration, and testing end to end.
01Process snapshot
02Agent specifications
Reads the incoming HubSpot contact record the moment a demo-interest signal fires, scores the lead against role, company size, and traffic source, and outputs either a discovery-call Calendly URL or a full-demo Calendly URL. When the computed confidence score falls below the configured threshold the agent writes a REVIEW_REQUIRED flag to the contact record and halts, routing the lead to a rep for manual assessment before any outbound email is sent.
// Input contact.id : string // HubSpot contact record ID contact.jobtitle : string // e.g. 'Head of Sales', 'Founder' contact.company : string // Company name contact.num_employees : integer // Company size band contact.hs_lead_source: string // e.g. 'Organic Search', 'Outbound Sequence' deal.id : string // Associated deal record ID (if exists) // Scoring logic (internal) score += 30 if jobtitle matches [VP|Director|Head|Founder|Owner] score += 25 if num_employees >= 10 score += 20 if hs_lead_source in ['Referral','Outbound Sequence'] score += 15 if deal.amount > 0 // Threshold: score >= 50 -> auto-route; score < 50 -> REVIEW_REQUIRED // Output (auto-route path) routing.decision : 'DISCOVERY' | 'FULL_DEMO' routing.calendly_url : string // Segment-specific Calendly booking URL routing.contact_id : string // Passed to Scheduling and Confirmation Agent routing.deal_id : string // Passed downstream // Output (manual review path) routing.decision : 'REVIEW_REQUIRED' hubspot.contact.review_flag : true // Property written back to HubSpot
- HubSpot tier must be at least Sales Hub Starter to access the Workflows API and custom contact properties. Confirm the active subscription tier before build begins.
- The two Calendly booking URLs (discovery-call and full-demo) must be created and tested in Calendly before they are hardcoded into the routing output. Name them consistently: 'discovery-call' and 'full-demo' in the Calendly event slug.
- The REVIEW_REQUIRED HubSpot contact property must be created as a boolean field in HubSpot before the agent can write to it. Property internal name: hs_review_required.
- Scoring weights are defaults. The sales lead must confirm or adjust role, company-size, and source weights before the agent goes live.
- Dedupe behaviour: if a contact already has an active open deal in stage 'Demo Booked' the agent must detect this and exit without creating a duplicate workflow run. Check deal stage before scoring.
- Fallback: if HubSpot returns a 429 rate-limit error, the agent must wait 10 seconds and retry up to three times before raising a logged error and halting.
Fires immediately after the Lead Qualification Agent emits a valid routing decision. Sends the appropriate Calendly booking link to the prospect via a personalised Gmail message. If no Calendly booking event is detected within 48 hours, a single follow-up nudge is sent. When Calendly posts a booking-confirmed webhook, the agent creates a Zoom meeting under the assigned rep's account, generates the unique join link, and adds it to a Google Calendar invite shared with both the prospect and the rep.
// Input (from Lead Qualification Agent) routing.decision : 'DISCOVERY' | 'FULL_DEMO' routing.calendly_url : string routing.contact_id : string routing.deal_id : string contact.email : string contact.firstname : string rep.email : string // Assigned rep Gmail address rep.zoom_user_id : string // Zoom user ID for meeting creation // Output (booking link sent) gmail.sent_message_id : string // Gmail message ID of booking email workflow.nudge_timer_started: true // 48-hour countdown initiated // Input (Calendly webhook payload on booking confirmed) event.calendly_event_uri : string event.start_time : ISO8601 datetime event.end_time : ISO8601 datetime invitee.name : string invitee.email : string // Output (after booking confirmed) zoom.meeting_id : string zoom.join_url : string zoom.start_url : string gcal.event_id : string // Google Calendar event ID gcal.invite_link : string booking.confirmed_at : ISO8601 datetime booking.meeting_start : ISO8601 datetime // Passed to Pre-Call Comms Agent
- Gmail API scope required: https://www.googleapis.com/auth/gmail.send. Do not request broader scopes. Authenticate using the rep's Google Workspace service account or individual OAuth, confirmed per rep before build.
- Calendly webhook must be registered at the organisation level with the event type 'invitee.created' subscribed. Confirm Calendly plan tier: webhook subscriptions require Calendly Standard or above.
- Zoom Meetings API requires user-level OAuth if meetings must appear under the rep's Zoom account (not the admin account). Each rep must individually authorise via Zoom OAuth during setup. If multi-rep routing is not in scope for this build, a single admin OAuth token may be used as a short-term substitute, but flag this clearly in documentation.
- The 48-hour nudge timer must be implemented as a delayed branch in the orchestration layer, not a polling loop. If Calendly confirms a booking before the 48 hours expire, the nudge branch must be cancelled immediately to avoid sending a follow-up after the prospect has already booked.
- Google Calendar invite must include the Zoom join URL in both the description field and the location field for maximum compatibility with calendar clients.
- Fallback: if Zoom API returns an error during meeting creation, log the failure to the error table, alert via Slack to the build monitoring channel, and send the calendar invite without a Zoom link, inserting a placeholder 'Zoom link to follow' in the description.
Fires when the Scheduling and Confirmation Agent passes a confirmed booking payload. Updates the HubSpot deal record to the demo-booked stage and logs the meeting date. At 24 hours before the meeting start time, dispatches a personalised pre-call brief to the prospect via Gmail, pulling the prospect's name, company, and meeting topic from HubSpot properties. Simultaneously posts a formatted notification to the designated sales Slack channel. At 60 minutes before the meeting, sends a short reminder email to the prospect containing the Zoom join link.
// Input (from Scheduling and Confirmation Agent)
booking.confirmed_at : ISO8601 datetime
booking.meeting_start : ISO8601 datetime
zoom.join_url : string
gcal.event_id : string
routing.deal_id : string
routing.contact_id : string
contact.firstname : string
contact.email : string
contact.company : string
rep.email : string
rep.slack_user_id : string // For @mention in Slack message
// On CRM update (immediate, step 1 of agent)
hubspot.deal.dealstage : 'Demo Booked' // Internal stage ID must be confirmed
hubspot.deal.demo_date : ISO8601 date // Custom property: demo_date
hubspot.deal.zoom_link : string // Custom property: zoom_join_url
// On pre-call brief send (T-24h)
gmail.brief_subject : string // e.g. 'Getting ready for your demo, {{firstname}}'
gmail.brief_body : string // Templated HTML, tokens: firstname, company, meeting_start, zoom_join_url
gmail.sent_message_id_brief : string
// On Slack notification (immediate)
slack.channel : string // e.g. '#sales-demos'
slack.message_blocks : JSON // Prospect name, company, time, rep @mention, Zoom link
// On reminder send (T-60min)
gmail.reminder_subject : string // e.g. 'Your demo starts in 60 minutes'
gmail.reminder_body : string // Zoom join link + one-line agenda
gmail.sent_message_id_reminder: string
// On approval (ambiguous lead flagged by Qualification Agent)
// Rep reviews contact in HubSpot, clears hs_review_required flag
// Workflow resumes from Scheduling and Confirmation Agent with rep-confirmed routing decision- The HubSpot deal stage ID for 'Demo Booked' must be confirmed from the client's HubSpot pipeline configuration before this agent can write the stage update. Stage IDs are pipeline-specific and cannot be assumed. Retrieve via GET /crm/v3/pipelines/deals.
- Two custom HubSpot deal properties must be created before build: demo_date (date field) and zoom_join_url (single-line text). Confirm these do not already exist under different internal names to avoid duplicates.
- The pre-call brief HTML template must be signed off by the sales lead before the agent is built. Personalisation tokens are: {{firstname}}, {{company}}, {{meeting_start}} (formatted as readable datetime), and {{zoom_join_url}}. If any HubSpot property is blank, the template must degrade gracefully, substituting 'your company' for a missing company value rather than rendering a broken token.
- Slack integration method: use the Slack Incoming Webhook URL for simplicity at this build scale. If the client's Slack workspace restricts webhook creation, fall back to the Web API with a bot token and the chat:write scope.
- The T-24h and T-60min timers must both be calculated from the confirmed booking.meeting_start value, not from the time the agent runs. Use scheduled execution with an exact datetime target, not relative delays, to avoid drift.
- If the meeting is booked within 24 hours of the start time, the pre-call brief must be sent immediately rather than waiting for T-24h. Add a conditional check: if (meeting_start - now) < 24h then send brief immediately.
03End-to-end data flow
// ============================================================
// DEMO AND DISCOVERY SCHEDULING: END-TO-END DATA FLOW
// ============================================================
// TRIGGER
// HubSpot contact lifecycle stage changes to 'Demo Requested'
// OR HubSpot form submission mapped to demo-interest workflow
// Fields available at trigger:
// contact.id, contact.email, contact.firstname, contact.lastname
// contact.jobtitle, contact.company, contact.num_employees
// contact.hs_lead_source, deal.id, deal.amount
// ============================================================
// AGENT 1: Lead Qualification Agent
// ============================================================
INPUT <- HubSpot Contacts API GET /crm/v3/objects/contacts/{contact.id}
INPUT <- HubSpot Deals API GET /crm/v3/objects/deals/{deal.id}
SCORE = compute(contact.jobtitle, contact.num_employees,
contact.hs_lead_source, deal.amount)
IF score >= 50:
routing.decision = 'DISCOVERY' | 'FULL_DEMO'
routing.calendly_url = select_url(routing.decision)
// e.g. 'https://calendly.com/[YourCompany.com]/discovery-call'
// or 'https://calendly.com/[YourCompany.com]/full-demo'
PASS -> Agent 2 with: routing.decision, routing.calendly_url,
contact.id, contact.email, contact.firstname,
deal.id, rep.email, rep.zoom_user_id
IF score < 50:
hubspot.contact.hs_review_required = true
HubSpot Contacts API PATCH /crm/v3/objects/contacts/{contact.id}
HALT // Rep reviews; on manual clearance workflow resumes to Agent 2
// ============================================================
// AGENT 2: Scheduling and Confirmation Agent
// ============================================================
// STEP 2a: Send booking link
INPUT <- routing.calendly_url, contact.email, contact.firstname, rep.email
OUTPUT -> Gmail API POST /gmail/v1/users/me/messages/send
to: contact.email
from: rep.email
subject: 'Book your {routing.decision} with [YourCompany.com]'
body: personalised HTML with routing.calendly_url embedded
gmail.sent_message_id = response.id
// STEP 2b: 48-hour non-response branch
TIMER = now() + 48h
WAIT // Monitor for Calendly booking_confirmed webhook
IF no booking_confirmed received within 48h:
OUTPUT -> Gmail API POST /gmail/v1/users/me/messages/send
to: contact.email
subject: 'Still happy to connect?'
body: nudge email with routing.calendly_url
// Single nudge only; no further automated follow-up
// STEP 2c: Calendly booking confirmed
INPUT <- Calendly webhook POST {platform_webhook_endpoint}
event.calendly_event_uri, event.start_time, event.end_time
invitee.name, invitee.email
// STEP 2d: Create Zoom meeting
OUTPUT -> Zoom Meetings API POST /v2/users/{rep.zoom_user_id}/meetings
body: { topic, start_time: event.start_time,
duration: (event.end_time - event.start_time) in minutes,
type: 2 }
zoom.meeting_id = response.id
zoom.join_url = response.join_url
zoom.start_url = response.start_url
// STEP 2e: Create Google Calendar invite
OUTPUT -> Google Calendar API POST /calendar/v3/calendars/primary/events
body: { summary, start: event.start_time, end: event.end_time,
attendees: [contact.email, rep.email],
location: zoom.join_url,
description: 'Zoom link: ' + zoom.join_url }
gcal.event_id = response.id
gcal.invite_link = response.htmlLink
// PASS confirmed booking payload to Agent 3
PASS -> Agent 3 with: booking.meeting_start = event.start_time,
zoom.join_url, gcal.event_id,
routing.deal_id, routing.contact_id,
contact.firstname, contact.email, contact.company,
rep.email, rep.slack_user_id
// ============================================================
// AGENT 3: Pre-Call Comms Agent
// ============================================================
// STEP 3a: Update HubSpot deal (immediate)
INPUT <- routing.deal_id, booking.meeting_start, zoom.join_url
OUTPUT -> HubSpot Deals API PATCH /crm/v3/objects/deals/{deal.id}
properties: { dealstage: '{demo_booked_stage_id}',
demo_date: booking.meeting_start,
zoom_join_url: zoom.join_url }
// STEP 3b: Post Slack notification (immediate)
OUTPUT -> Slack Incoming Webhook POST {slack_webhook_url}
payload: { blocks: [
{ type: 'section', text: '*New demo booked*' },
{ type: 'section', text:
'Prospect: ' + contact.firstname + ' ' + contact.lastname +
' | Company: ' + contact.company +
' | Time: ' + booking.meeting_start +
' | Rep: <@' + rep.slack_user_id + '>' +
' | Zoom: ' + zoom.join_url } ] }
// STEP 3c: Send pre-call brief (at booking.meeting_start - 24h)
TIMER = booking.meeting_start - 24h
// If (meeting_start - now) < 24h: send immediately
OUTPUT -> Gmail API POST /gmail/v1/users/me/messages/send
to: contact.email
subject: 'Getting ready for your demo, {{firstname}}'
body: HTML template with tokens:
{{firstname}} <- contact.firstname
{{company}} <- contact.company
{{meeting_start}} <- formatted datetime
{{zoom_join_url}} <- zoom.join_url
gmail.sent_message_id_brief = response.id
// STEP 3d: Send pre-call reminder (at booking.meeting_start - 60min)
TIMER = booking.meeting_start - 60min
OUTPUT -> Gmail API POST /gmail/v1/users/me/messages/send
to: contact.email
subject: 'Your demo starts in 60 minutes'
body: zoom.join_url + one-line agenda
gmail.sent_message_id_reminder = response.id
// ============================================================
// END OF FLOW
// All steps completed without rep involvement (except manual
// review of REVIEW_REQUIRED leads before Agent 2 fires).
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Demo & Discovery Scheduling.