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
Appointment / Booking Management
[YourCompany.com] · Operations Department · Prepared by FullSpec · [Today's Date]
This document is the primary technical reference for the FullSpec team building the Appointment and Booking Management automation. It covers the current-state process map, all three agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. Everything the FullSpec team needs to begin implementation without further clarification is contained here. Where an item requires confirmation before build can proceed, it is called out explicitly in the relevant agent block.
01Process snapshot
02Agent specifications
Handles all inbound booking requests arriving via the Calendly booking page or an embedded web form. The agent checks real-time availability through Calendly, routes the request based on slot availability, creates the confirmed Google Calendar event with appropriate buffer periods, and sends a branded confirmation email via Gmail. This agent eliminates the manual availability check and client reply loop that currently account for 18 minutes per booking. It is the entry point for the entire automation and must produce a clean, structured booking record before passing control to the CRM and Payment Agent.
// Input
calendly.event.created webhook payload {
event_uuid: string,
event_type_name: string,
start_time: ISO8601,
end_time: ISO8601,
invitee.name: string,
invitee.email: string,
invitee.timezone: string,
questions_and_answers: [ { question: string, answer: string } ]
}
// Output
google_calendar.event.created {
calendar_id: 'primary',
summary: '{invitee.name} - {event_type_name}',
start: { dateTime: start_time, timeZone: invitee.timezone },
end: { dateTime: end_time_plus_buffer, timeZone: invitee.timezone },
description: 'Auto-created by FullSpec Booking Automation',
attendees: [ { email: invitee.email } ]
}
gmail.message.sent {
to: invitee.email,
subject: 'Your booking is confirmed - {event_type_name}',
body: confirmation_template rendered with booking_details
}
booking_record passed to CRM and Payment Agent {
invitee_name: string,
invitee_email: string,
invitee_phone: string | null,
event_type_name: string,
start_time: ISO8601,
end_time: ISO8601,
calendar_event_id: string,
deposit_required: boolean
}- Calendly plan must support webhook event notifications (Teams plan or above at $16/month confirmed in tooling). Verify that the 'invitee.created' webhook event is enabled on the correct event type before build.
- Google Calendar must be the single authoritative calendar for availability. If multiple staff calendars are in use, confirm which calendar_id is the source of truth before build. The automation writes to 'primary' by default.
- Buffer time (before and after each appointment) must be configured in Calendly directly. The automation reads the end_time from the Calendly payload, which already includes the buffer if set in Calendly event settings.
- The Gmail confirmation template must be provided by the business owner as branded HTML before the send node is built. A plain-text fallback must also be prepared.
- Invitee phone number is captured via a Calendly custom question field. Confirm the exact question label in the Calendly form so the parser maps it to invitee_phone correctly.
- Deposit required flag is determined by a lookup against the event_type_name. A mapping table (event type to deposit true/false) must be confirmed with the business owner before build and stored as a configuration variable in the orchestration layer.
- Deduplication: if a Calendly webhook fires twice for the same event_uuid (retry scenario), the agent checks whether a Google Calendar event with that uuid in its description already exists before creating a new one. If found, it skips creation and logs a duplicate warning.
Receives the structured booking record from the Booking Intake Agent and handles all CRM logging and conditional payment collection. The agent performs a HubSpot contact lookup by email, creates the contact if not found, and creates or updates a deal record linked to the contact with the booking date, service type, and status. If deposit_required is true, it generates a Stripe payment link and sends it to the client via Gmail, then listens for the Stripe payment_intent.succeeded webhook to mark the booking as payment-confirmed in HubSpot. If deposit_required is false, the booking is marked confirmed immediately and control passes to the Reminder and Follow-Up Agent.
// Input
booking_record {
invitee_name: string,
invitee_email: string,
invitee_phone: string | null,
event_type_name: string,
start_time: ISO8601,
calendar_event_id: string,
deposit_required: boolean
}
// On approval (deposit path)
stripe.payment_intent.succeeded webhook {
payment_intent_id: string,
amount_received: integer (cents),
customer_email: string,
metadata.booking_calendar_event_id: string
}
// Output
hubspot.contact.upserted {
contact_id: string,
email: invitee_email,
firstname: invitee_name (first token),
lastname: invitee_name (remaining tokens),
phone: invitee_phone | null
}
hubspot.deal.created {
deal_id: string,
dealname: '{event_type_name} - {invitee_name} - {start_time date}',
dealstage: 'appointment_confirmed' | 'awaiting_deposit',
appointment_date: start_time,
service_type: event_type_name,
payment_status: 'confirmed' | 'pending' | 'not_required',
calendar_event_id: string
}
stripe.payment_link.sent (deposit path) {
payment_link_url: string,
amount: deposit_amount_cents,
metadata.booking_calendar_event_id: calendar_event_id
}
gmail.message.sent (deposit path) {
to: invitee_email,
subject: 'Complete your booking deposit - {event_type_name}',
body: deposit_template rendered with payment_link_url
}
confirmed_booking_record passed to Reminder and Follow-Up Agent {
contact_id: string,
deal_id: string,
invitee_email: string,
invitee_phone: string | null,
start_time: ISO8601,
calendar_event_id: string
}- HubSpot tier must support Deals API and custom deal properties (Starter CRM or above at $50/month confirmed in tooling). Verify that custom properties 'appointment_date', 'service_type', 'payment_status', and 'calendar_event_id' exist or create them during the discovery stage.
- HubSpot contact lookup uses email as the unique key. If a contact exists, update it. If not, create it. Never create duplicate contacts. Use the HubSpot search API (POST /crm/v3/objects/contacts/search) with filter on 'email' property before any create call.
- Stripe account must be verified and in live mode before deposit links can be sent to real clients. Test mode is used during QA. Confirm deposit amount per event type with the business owner. Store deposit amounts in the same event-type configuration table as the deposit_required flag.
- Stripe payment link metadata must include calendar_event_id so the payment_intent.succeeded webhook can match the payment back to the correct booking record. This is the primary correlation key.
- If a deposit payment is not received within a configurable window (default 24 hours), the agent sends a single payment reminder email and logs a 'deposit_overdue' status on the HubSpot deal. It does not cancel the booking automatically. Escalation to the operations team is logged via error logging (see Section 04).
- Gmail deposit template must be provided by the business owner before this agent is built. The template must include the payment_link_url variable token.
- HubSpot deal pipeline and stage IDs must be confirmed during discovery. The automation writes to the pipeline ID, not the stage name string, to avoid breakage if the business renames stages.
Manages all time-based communication for confirmed bookings. The agent schedules two reminder touchpoints per booking: one 48 hours before the appointment via both Twilio SMS and Gmail, and one 2 hours before the appointment via the same two channels. After the appointment window closes, the agent checks the HubSpot deal for a completion status. If the deal has not been updated to 'appointment_completed' within 30 minutes of the scheduled end time, the agent triggers the no-show branch: it sends a follow-up email via Gmail and updates the HubSpot deal status to 'no_show'. Complex reschedule disputes and refund decisions are flagged to the operations team and not handled by the automation.
// Input
confirmed_booking_record {
contact_id: string,
deal_id: string,
invitee_email: string,
invitee_phone: string | null,
start_time: ISO8601,
calendar_event_id: string
}
// Scheduled reminder jobs created at input time
reminder_job_48h: { execute_at: start_time minus 48 hours }
reminder_job_2h: { execute_at: start_time minus 2 hours }
noshow_check_job: { execute_at: start_time plus appointment_duration_minutes plus 30 min }
// Output (reminder path - executed at each scheduled time)
twilio.sms.sent {
to: invitee_phone (E.164 format),
from: twilio_registered_number,
body: reminder_sms_template rendered with { appointment_time, service_type }
}
gmail.message.sent {
to: invitee_email,
subject: 'Reminder: your appointment is coming up',
body: reminder_email_template rendered with booking_details
}
// Output (no-show path - executed by noshow_check_job if deal not updated)
hubspot.deal.updated {
deal_id: string,
dealstage: 'no_show',
no_show_date: ISO8601
}
gmail.message.sent {
to: invitee_email,
subject: 'We missed you today - let us rebook',
body: noshow_followup_template rendered with { rebooking_link, invitee_name }
}- Twilio account must have a registered sending number before build. For US-based SMS at volume, A2P 10DLC registration is required. This registration can take 2 to 4 weeks and must be initiated immediately after discovery is complete. Do not block build on this but confirm the timeline with the business owner.
- If invitee_phone is null (not captured at booking), the SMS step is skipped gracefully and only the Gmail reminder is sent. Log a warning to the error log but do not fail the run.
- All reminder and no-show jobs must be stored as scheduled tasks in the orchestration layer's job queue with deal_id as the correlation key. If a booking is cancelled in Calendly before the reminder fires, a Calendly invitee.cancelled webhook must trigger cancellation of the corresponding pending jobs.
- No-show detection reads the HubSpot deal stage via the HubSpot Deals API (GET /crm/v3/objects/deals/{deal_id}). If dealstage equals 'appointment_completed', no action is taken. If dealstage is any other value after the noshow_check_job fires, the no-show branch executes.
- SMS and email reminder templates must be provided by the business owner before this agent is built. The Twilio template body must be under 160 characters for a single SMS segment.
- Appointment duration in minutes must be stored as a configuration variable per event type to calculate the noshow_check_job execution time correctly.
- Twilio rate limit is 1 message per second per long-code number. For volumes above 120 bookings per month the current rate is well within limits, but a short-code or toll-free number should be evaluated if volume grows significantly.
03End-to-end data flow
// ============================================================
// END-TO-END DATA FLOW: Appointment / Booking Management
// FullSpec Automation Platform
// ============================================================
// TRIGGER
// Client submits booking via Calendly link or embedded web form
INBOUND: calendly.webhook [event: invitee.created]
-> event_uuid: string
-> event_type_name: string
-> start_time: ISO8601
-> end_time: ISO8601
-> invitee.name: string
-> invitee.email: string
-> invitee.timezone: string
-> questions_and_answers[phone]: string | null
// ============================================================
// AGENT 1: Booking Intake Agent
// ============================================================
STEP 1.1: Parse Calendly payload
-> invitee_name = invitee.name
-> invitee_email = invitee.email
-> invitee_phone = questions_and_answers[label='Phone number'].answer | null
-> invitee_timezone = invitee.timezone
-> start_time = event.start_time
-> end_time = event.end_time // includes Calendly buffer if configured
-> event_type_name = event.event_type.name
-> event_uuid = event.uuid
STEP 1.2: Deduplication check
QUERY google_calendar.events (description contains event_uuid)
IF found -> SKIP creation, LOG warning 'duplicate_event_uuid', EXIT agent
IF not found -> CONTINUE
STEP 1.3: Create Google Calendar event
POST google_calendar.events
-> calendar_id = config.primary_calendar_id
-> summary = '{invitee_name} - {event_type_name}'
-> start.dateTime = start_time
-> end.dateTime = end_time
-> start.timeZone = invitee_timezone
-> description = 'FullSpec auto-created | uuid:{event_uuid}'
-> attendees = [ invitee_email ]
RETURN calendar_event_id
STEP 1.4: Send confirmation email via Gmail
POST gmail.messages.send
-> to = invitee_email
-> subject = 'Your booking is confirmed - {event_type_name}'
-> body = render(confirmation_email_template, { invitee_name, start_time,
event_type_name, invitee_timezone })
STEP 1.5: Lookup deposit_required flag
QUERY config.event_type_deposit_map[event_type_name]
-> deposit_required: boolean
-> deposit_amount_cents: integer | null
// AGENT HANDOFF 1 -> 2
EMIT booking_record {
invitee_name, invitee_email, invitee_phone,
event_type_name, start_time, end_time,
calendar_event_id, deposit_required, deposit_amount_cents
}
// ============================================================
// AGENT 2: CRM and Payment Agent
// ============================================================
STEP 2.1: HubSpot contact upsert
POST /crm/v3/objects/contacts/search
-> filterGroups[0].filters[0].propertyName = 'email'
-> filterGroups[0].filters[0].value = invitee_email
IF contact found -> contact_id = results[0].id
IF not found ->
POST /crm/v3/objects/contacts
-> email = invitee_email
-> firstname = invitee_name.split(' ')[0]
-> lastname = invitee_name.split(' ').slice(1).join(' ')
-> phone = invitee_phone | null
RETURN contact_id
STEP 2.2: HubSpot deal create
POST /crm/v3/objects/deals
-> dealname = '{event_type_name} - {invitee_name} - {start_time.date}'
-> pipeline = config.hubspot_pipeline_id
-> dealstage = 'awaiting_deposit' IF deposit_required ELSE 'appointment_confirmed'
-> appointment_date = start_time
-> service_type = event_type_name
-> payment_status = 'pending' IF deposit_required ELSE 'not_required'
-> calendar_event_id = calendar_event_id
RETURN deal_id
STEP 2.3: Associate deal to contact
PUT /crm/v3/objects/deals/{deal_id}/associations/contacts/{contact_id}/deal_to_contact
STEP 2.4: Deposit branch (IF deposit_required = true)
POST stripe.paymentLinks.create
-> line_items[0].price = config.stripe_price_id[event_type_name]
-> line_items[0].quantity = 1
-> metadata.calendar_event_id = calendar_event_id
-> metadata.deal_id = deal_id
-> after_completion.type = 'redirect'
RETURN payment_link_url
POST gmail.messages.send
-> to = invitee_email
-> subject = 'Complete your booking deposit - {event_type_name}'
-> body = render(deposit_email_template, { payment_link_url, invitee_name })
AWAIT stripe.webhook [event: payment_intent.succeeded]
-> MATCH metadata.calendar_event_id = calendar_event_id
-> PATCH /crm/v3/objects/deals/{deal_id}
-> dealstage = 'appointment_confirmed'
-> payment_status = 'confirmed'
-> payment_amount = amount_received_cents / 100
// AGENT HANDOFF 2 -> 3
EMIT confirmed_booking_record {
contact_id, deal_id, invitee_email, invitee_phone,
start_time, end_time, calendar_event_id,
appointment_duration_minutes = config.event_type_duration_map[event_type_name]
}
// ============================================================
// AGENT 3: Reminder and Follow-Up Agent
// ============================================================
STEP 3.1: Schedule reminder jobs
SCHEDULE reminder_job_48h AT (start_time - 48h)
SCHEDULE reminder_job_2h AT (start_time - 2h)
SCHEDULE noshow_check AT (start_time + appointment_duration_minutes + 30 min)
STORE { deal_id, calendar_event_id } as job metadata
STEP 3.2: Register Calendly cancellation listener
LISTEN calendly.webhook [event: invitee.cancelled, uuid: event_uuid]
IF fired -> CANCEL all pending jobs for this deal_id
-> PATCH hubspot.deal { dealstage: 'cancelled' }
-> LOG 'booking_cancelled'
STEP 3.3: Execute reminder_job_48h
IF invitee_phone != null:
POST twilio.messages.create
-> to = invitee_phone (E.164)
-> from = config.twilio_sending_number
-> body = render(reminder_sms_48h_template, { start_time, event_type_name })
POST gmail.messages.send
-> to = invitee_email
-> subject = 'Reminder: your appointment is tomorrow'
-> body = render(reminder_email_48h_template, { invitee_name, start_time })
STEP 3.4: Execute reminder_job_2h
IF invitee_phone != null:
POST twilio.messages.create
-> body = render(reminder_sms_2h_template, { start_time, event_type_name })
POST gmail.messages.send
-> subject = 'Reminder: your appointment is in 2 hours'
-> body = render(reminder_email_2h_template, { invitee_name, start_time })
STEP 3.5: Execute noshow_check
GET /crm/v3/objects/deals/{deal_id}?properties=dealstage
IF dealstage = 'appointment_completed' -> EXIT (attended)
IF dealstage != 'appointment_completed':
PATCH /crm/v3/objects/deals/{deal_id}
-> dealstage = 'no_show'
-> no_show_date = NOW()
POST gmail.messages.send
-> to = invitee_email
-> subject = 'We missed you today - let us rebook'
-> body = render(noshow_followup_template, { invitee_name, rebooking_link })
// END OF FLOW04Recommended build stack
More documents for this process
Every document generated for Appointment / Booking Management.