MotusAI
Stage 04 →← Stage 02

Stage 03 of 05

Scheduling
Bot

HOT leads get 3 real calendar slots emailed within seconds of qualification. One click books the meeting, creates the Google Meet link, and fires the Payment Bot.

Google Calendar APIMagic-link slot selectionfreeBusy conflict guardAuto Meet linkESC-SCH-001/002SMTP email

The Pipeline

10 steps from HOT lead to booked meeting

The Scheduling Bot (B3) is a BullMQ worker on bot-scheduling. It only processes scheduling.book jobs — a name guard at line 1 skips everything else.

01

Trigger: scheduling.book

The Qualification Bot enqueues this job the moment a lead scores HOT (≥ 75). The Scheduling Bot picks it from the bot-scheduling BullMQ queue — a job-name guard ensures it ignores everything else.

02

Fetch lead from DB

Read the full lead record from Postgres — email, name, company, BANT score. All context was already captured by Intake and scored by Qualification. No re-extraction.

03

Google OAuth token refresh

Exchange the stored refresh token for a short-lived access token via oauth2.googleapis.com. The token lasts 3600 seconds — refreshed on every job to avoid expiry mid-flow.

04

freeBusy calendar query

Call Google Calendar API freeBusy.query for the sales rep's calendar over the next 7 days. Returns all booked time blocks. If this call fails → ESC-SCH-002.

05

Generate 3 free 45-min slots

Invert the busy blocks against business hours (9:00–17:00, Mon–Fri) over the next 5 business days. Pick the first 3 clean 45-minute windows. Slot hours: 9, 10, 11, 13, 14, 15, 16.

06

DB write: AWAITING_SLOT

Atomically write status=AWAITING_SLOT, pendingSlots (JSON array of start/end ISO strings), slotSentAt=now() to the Lead row. Audit log entry written.

07

Send slot-selection email

SMTP email to the lead with 3 clickable magic links — one per slot. Each link hits GET /api/integrations/scheduling/confirm?lead=ID&slot=N. No login required. Clicking = confirmed.

08

Lead clicks a magic link

The NestJS confirm endpoint validates the lead, checks the slot is still free (ESC-SCH-001 race guard), then calls Google Calendar events.insert with conferenceDataVersion=1 to generate a real Meet link.

09

DB write: BOOKED

Update Lead: meetingBooked=true, meetingTime, meetingLink (real Google Meet URL), status=BOOKED, pendingSlots=null. The lead sees a confirmation HTML page with the Meet link.

10

Enqueue Payment Bot

Add payment.request job to bot-payment queue. The Payment Bot (B4) takes over — generates a Stripe checkout link and emails it to the lead within seconds of meeting confirmation.

Google Calendar Integration

3 API calls, full meeting lifecycle

No third-party scheduling SDK — direct Google Calendar REST API calls using OAuth 2.0. The refresh token is stored in env; a short-lived access token is minted per-job.

Token

POST oauth2.googleapis.com/token

Exchange refresh_token → access_token (3600s TTL). Uses GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET + GOOGLE_REFRESH_TOKEN.

FreeBusy

POST calendar/v3/freeBusy

Query busy blocks for SALES_REP_EMAIL over next 7 days. Returns array of {start, end} ISO strings. Used to generate conflict-free slots.

Create Event

POST calendar/v3/calendars/primary/events?conferenceDataVersion=1

Insert event with lead + sales rep as attendees. conferenceDataVersion=1 auto-generates a Google Meet link in conferenceData.entryPoints.

// Slot generation — bots/scheduling/src/index.ts

// 1. Refresh token → access token
const res = await fetch('https://oauth2.googleapis.com/token', {
  method: 'POST',
  body: new URLSearchParams({
    client_id: process.env.GOOGLE_CLIENT_ID,
    client_secret: process.env.GOOGLE_CLIENT_SECRET,
    refresh_token: process.env.GOOGLE_REFRESH_TOKEN,
    grant_type: 'refresh_token',
  }),
})
const { access_token } = await res.json()

// 2. freeBusy query — 7-day window
const busy = await fetch('https://www.googleapis.com/calendar/v3/freeBusy', {
  method: 'POST',
  headers: { Authorization: `Bearer ${access_token}` },
  body: JSON.stringify({
    timeMin: now.toISOString(),
    timeMax: sevenDaysLater.toISOString(),
    items: [{ id: process.env.SALES_REP_EMAIL }],
  }),
})

// 3. Invert busy blocks → 3 free 45-min slots
const slots = generateFreeSlots(busyTimes)  // next 5 biz days, hours: 9-16

The Slot Email

Magic links — no login, no forms

The lead receives an HTML email with 3 pre-built booking links. Clicking any link immediately confirms that slot — no redirect to a booking page, no account creation. The confirmation is instant.

The magic link is a GET request so it works directly from any email client. The URL encodes both the lead ID and the slot index — no session or cookie needed.

email templateSMTP → lead email
<div style="font-family: sans-serif; max-width: 600px; margin: auto; padding: 20px;">
  <h2 style="color: #2563eb;">Hi {leadName},</h2>
  <p>You've been identified as a HOT lead! Let's schedule a 45-minute discovery call.</p>
  <p>Pick any open time below to instantly confirm:</p>

  <!-- Slot 1 -->
  <a href="{apiBase}/integrations/scheduling/confirm?lead={leadId}&slot=0"
     style="display:inline-block; padding:12px 24px; background:#2563eb;
            color:white; border-radius:6px; margin-bottom:12px;">
    Monday, July 7 · 9:00 AM UTC
  </a>

  <!-- Slot 2 -->
  <a href="{apiBase}/integrations/scheduling/confirm?lead={leadId}&slot=1" ...>
    Monday, July 7 · 10:00 AM UTC
  </a>

  <!-- Slot 3 -->
  <a href="{apiBase}/integrations/scheduling/confirm?lead={leadId}&slot=2" ...>
    Tuesday, July 8 · 9:00 AM UTC
  </a>

  <p style="color:#666; font-size:0.9em;">
    Clicking any link immediately confirms the slot and generates a Google Meet invite.
  </p>
</div>

The Confirm Endpoint

GET /api/integrations/scheduling/confirm

NestJS handles the magic link click. It validates the lead, checks for slot conflicts, creates the calendar event, updates the DB, and renders an HTML confirmation page — all in a single synchronous request.

// apps/api/src/modules/integrations/integrations.service.ts

// GET /api/integrations/scheduling/confirm?lead=LEAD_ID&slot=N

1. Find lead by ID — 404 if not found
2. Guard: if status === 'BOOKED' → show "Already scheduled" page
3. Guard: if pendingSlots === null → show "Slots expired" page
4. Select slot[N] from pendingSlots JSON
5. ESC-SCH-001: freeBusy check — is this slot still free?
   → If BUSY: re-enqueue scheduling.book, show "Slot taken" page
6. POST /calendar/v3/calendars/primary/events?conferenceDataVersion=1
   → attendees: [leadEmail, salesRepEmail]
   → conferenceData.createRequest → Google Meet link
7. DB update:
   meetingBooked = true
   meetingTime   = slot.start
   meetingLink   = event.conferenceData.entryPoints[video].uri
   status        = 'BOOKED'
   pendingSlots  = null
8. Audit log: scheduling.meeting_booked
9. Enqueue: bot-payment / payment.request
10. Render success HTML with Meet link button

Happy path response

Renders a branded HTML page: "Meeting Confirmed!" with the Google Meet link as a blue button. The lead can click straight into the call from this page.

Error states

Each failure renders a distinct HTML page: "Already Scheduled", "Slot Conflict", "Slots Expired", "Invalid Request" — each with a clear explanation and next step.

DB Schema Changes

Fields added to the Lead model

Stage 03 adds 3 fields to track the scheduling state machine. The DB is synced via prisma db push — no migration files needed for dev.

fieldtypevalues / purposeadded in
statusLeadStatusAWAITING_SLOT | BOOKED | PENDINGStage 03
pendingSlotsJson?[{start: ISO, end: ISO}, ...]Stage 03
slotSentAtDateTime?timestamp when email was sentStage 03
meetingBookedBooleantrue after GCal event createdStage 01
meetingTimeDateTime?confirmed slot start timeStage 01
meetingLinkString?Google Meet URLStage 01

Lead status state machine

QUALIFIEDAWAITING_SLOTscheduling.book job picked up
AWAITING_SLOTBOOKEDLead clicks magic link → GCal event created
AWAITING_SLOTPENDINGESC-SCH-002: GCal API failure
AWAITING_SLOTAWAITING_SLOTESC-SCH-001: Slot conflict → re-enqueue

Environment Variables

10 vars required for Stage 03

All credentials live in .env at the repo root. The scheduling bot loads them via dotenv.config() at startup. NestJS loads them via ConfigModule.forRoot().

variablerequireddescription
GOOGLE_CLIENT_IDyesOAuth 2.0 client from Google Cloud Console
GOOGLE_CLIENT_SECRETyesOAuth 2.0 secret from Google Cloud Console
GOOGLE_REFRESH_TOKENyesLong-lived token from OAuth Playground (scope: calendar)
SALES_REP_EMAILyesGmail/Workspace address whose calendar is queried + booked into
SMTP_HOSTyesSMTP server hostname for sending slot emails
SMTP_PORTyes587 (STARTTLS) or 465 (SSL)
SMTP_USERyesSMTP authentication username
SMTP_PASSyesSMTP authentication password
EMAIL_FROMyesFrom address shown on slot-selection emails
NEXT_PUBLIC_API_BASE_URLyesBase URL for magic link confirm endpoint

GOOGLE_REFRESH_TOKEN expires in 7 days when the Google Cloud app is in Testing mode. Publish the OAuth consent screen to Production to get a non-expiring token. Go to: Cloud Console → APIs & Services → OAuth consent screen → Publish App.

Escalation Matrix

2 failure modes, both handled

ESC-SCH-001

Trigger: Lead clicks a slot that was just booked by another meeting

Action: Re-enqueue scheduling.book immediately. Lead sees "Slot taken — new options incoming". A fresh freeBusy query generates new slots and a new email is sent.

ESC-SCH-002

Trigger: Google Calendar API returns 5xx or token refresh fails

Action: Lead status reset to PENDING. BullMQ retries up to 4 times with exponential backoff. Audit log records escalation. Admin alert logged with full error context.

What Happens Next

Scheduling completes → Payment starts

The moment the meeting is confirmed the confirm endpoint enqueues a payment.request job on the bot-payment queue. The Payment Bot (B4) generates a Stripe checkout link and emails it to the lead within seconds.

Stage 01Intake Botdone
Stage 02Qualificationdone
Stage 03Schedulingactive
Stage 04Paymentnext
Stage 05Supportfuture

See it live

Submit a HOT lead — get a real slot email

Submit via the capture form. The Qualification Bot scores it HOT. Within seconds the Scheduling Bot emails 3 Google Calendar slots. Click one to confirm and get a real Meet link.