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
Interview Scheduling Automation
[YourCompany.com] · HR Department · Prepared by FullSpec · [Today's Date]
This document gives the FullSpec build team everything needed to construct, connect, and verify the Interview Scheduling automation from scratch. It covers the current manual process step map, the full specification for each automation agent, the end-to-end data flow with exact field names, and the recommended build stack with time estimates. The owner-facing runbook and QA plan are separate documents. This pack assumes familiarity with REST APIs, webhook configuration, and workflow automation tooling.
01Process snapshot
02Agent specifications
Reads interviewer panel configuration from a Notion database, queries all assigned Google Calendars via the Calendar API, and computes the first three non-overlapping windows that satisfy the required interview duration and participant constraints. Outputs a ranked slot list which is passed directly to the Scheduling and Comms Agent. No candidate-facing communication occurs in this agent. This agent carries the most logic-intensive work in the pipeline and must handle partial availability correctly (for example, when one interviewer has no free slots in the next 5 business days). Complexity rating: Complex.
// Input
greenhouse.candidate_id : string // e.g. 'cand_48291'
greenhouse.job_id : string // e.g. 'job_1024'
greenhouse.stage_name : string // e.g. 'Technical Interview'
greenhouse.candidate_email : string
greenhouse.candidate_name : string
// Notion lookup output
notion.panel_config.interviewers[] : array<string> // Google account emails
notion.panel_config.duration_minutes : integer // e.g. 60
notion.panel_config.interview_format : string // e.g. 'Video - Google Meet'
notion.panel_config.focus_area[] : array<string> // per interviewer
notion.panel_config.booking_window_days : integer // e.g. 7
// Google Calendar query output
calendar.free_slots[] : array<object>
.start_utc : ISO8601 datetime
.end_utc : ISO8601 datetime
.participants_available[] : array<string> // emails confirmed free
// Output to Scheduling and Comms Agent
slot_list.ranked[] : array<object> // top 3 slots
.slot_id : string // internal UUID
.start_utc : ISO8601 datetime
.end_utc : ISO8601 datetime
.interviewers[] : array<string>
.calendly_scope_window : object { start, end }- Notion workspace must have a structured database (not a freeform page) with at least the following properties: Stage Name (title), Interviewers (multi-select or relation), Duration Minutes (number), Interview Format (select), Focus Area (multi-select), Booking Window Days (number). The FullSpec team will confirm the exact property names before build and map them in the integration config.
- Google Calendar API requires the calendar.readonly OAuth scope for each interviewer account. Service account delegation is the preferred approach if the organisation uses Google Workspace. Confirm whether domain-wide delegation is enabled before build.
- Slot computation must enforce a minimum 15-minute buffer before and after each proposed slot to avoid back-to-back conflicts. This is a hard constraint, not configurable at runtime.
- If fewer than 1 valid overlapping slot is found within the booking window, the agent must route to the manual exception path and post a Slack alert to the recruiter channel rather than sending an empty or invalid Calendly link to the candidate.
- The Calendly scoping mechanism requires a Calendly Teams plan (minimum). Confirm the current Calendly plan tier before build. A Personal plan will not support the availability-window scoping required by this agent.
- Dedupe: if a Greenhouse webhook fires twice for the same candidate_id and stage_name within a 10-minute window (a known Greenhouse behaviour on some plan tiers), the second trigger must be suppressed. Implement a run-log check against candidate_id + stage_name + trigger_timestamp before executing the Notion lookup.
Receives the ranked slot list from the Availability Matching Agent and drives all candidate-facing and interviewer-facing communication through to a confirmed booking. Sends a scoped Calendly booking link to the candidate via Gmail, monitors for the Calendly booking webhook, creates the Google Calendar event with all participants on confirmation, fires per-interviewer Slack briefings, and schedules the day-before Gmail reminder timed to 20 hours before the interview start. Also handles the reschedule path: if Calendly fires a reschedule webhook, the agent re-triggers the calendar event update and resends all downstream notifications without recruiter involvement, unless the reschedule falls outside permitted windows. Complexity rating: Complex.
// Input (from Availability Matching Agent)
slot_list.ranked[] : array<object> // top 3 slots
candidate.email : string
candidate.name : string
candidate.greenhouse_id : string
interviewers[] : array<object>
.email : string
.name : string
.slack_user_id : string
.focus_area : string
.cv_link : string // Greenhouse candidate profile URL
interview.format : string // e.g. 'Video - Google Meet'
interview.duration_minutes : integer
// On Calendly booking confirmed
calendly.event.uri : string
calendly.event.start_time : ISO8601 datetime
calendly.event.end_time : ISO8601 datetime
calendly.invitee.email : string
calendly.invitee.name : string
calendly.event_type : string // 'invitee.created' | 'invitee.canceled'
// On reschedule
calendly.reschedule.old_event_uri : string
calendly.reschedule.new_start_time : ISO8601 datetime
calendly.reschedule.new_end_time : ISO8601 datetime
// Output
gmail.booking_link_sent : boolean
google_calendar.event_id : string
slack.briefing_sent[] : array<string> // slack_user_ids messaged
gmail.reminder_scheduled_at : ISO8601 datetime // 20 hrs before interview
comms_agent.confirmed_slot : object { start_utc, end_utc, event_id }- Calendly webhook subscription must be registered for both invitee.created and invitee.canceled event types. A third subscription for routing.form_submitted is optional but not required for this build.
- Gmail must send from a dedicated scheduling alias (e.g. scheduling@[YourCompany.com]) rather than a personal recruiter address. Confirm the alias exists and that the service account or OAuth credential has Gmail send-as permission for that alias before build.
- Google Calendar event creation requires the calendar.events scope. The event must include: summary (candidate name + role + stage), description (video link, prep notes, focus area per interviewer), attendees (candidate email + all interviewer emails), conferenceData request (Google Meet auto-generation), and reminders set to 0 (the automation manages its own reminder via Gmail, not native Calendar reminders).
- Slack briefings must be sent as direct messages to each interviewer's Slack user ID, not to a shared channel, to avoid leaking candidate details. The Slack bot must have the chat:write and users:read OAuth scopes. Slack user IDs must be pre-mapped to Google Calendar email addresses in the Notion panel config or a separate mapping table.
- The day-before Gmail reminder must be scheduled as a delayed action (use the orchestration platform's built-in scheduler or a time-based trigger) set to fire at interview_start_time minus 20 hours. If the interview is rescheduled, the existing scheduled reminder must be cancelled and a new one registered for the updated time.
- If the candidate does not book within 48 hours of the booking link being sent, the agent must fire a Slack alert to the recruiter and log the event as a manual exception. Do not send a second booking link automatically without recruiter approval.
- Reschedule path: if the new requested slot falls outside the originally computed available windows, route to the manual exception path rather than attempting a new Calendar query. The recruiter must explicitly trigger a fresh slot search from the Notion panel config.
Writes the confirmed interview date, time, panel member names, and interview format back to the candidate's Greenhouse record once a Calendly booking is confirmed or a reschedule is completed. This keeps the hiring pipeline current without manual data entry and ensures downstream pipeline reporting reflects the actual scheduled state. If the Greenhouse write fails (for example due to a permissions error on the API key), the agent must log the failure, alert the recruiter via Slack, and retry once after a 5-minute delay before marking the run as failed. Complexity rating: Simple.
// Input (from Scheduling and Comms Agent) candidate.greenhouse_id : string confirmed_slot.start_utc : ISO8601 datetime confirmed_slot.end_utc : ISO8601 datetime google_calendar.event_id : string interviewers[] : array<object> .greenhouse_user_id : string // must be resolved before build .name : string interview.format : string interview.stage_id : string // Greenhouse stage ID for this pipeline step // Output greenhouse.scheduled_interview_id : string // returned by Greenhouse on success greenhouse.write_status : 'success' | 'failed' greenhouse.error_code : string | null ats_update.logged_at : ISO8601 datetime
- Greenhouse API write-back uses the POST /v1/scheduled_interviews endpoint. The API key must have the permission 'Manage Interviews' enabled. Confirm this permission is active on the key provisioned during discovery before build begins.
- Greenhouse interviewer_user_ids (the internal Greenhouse integer IDs) must be pre-resolved from the recruiter's account and stored in the Notion panel config or a lookup table. The Google Calendar email address is not the same as the Greenhouse user ID, and there is no automatic cross-reference available via the Greenhouse API.
- If the Greenhouse plan is Basic or Essential, the scheduled_interviews write-back endpoint may not be available. Confirm the plan tier includes API access to this endpoint before build. If unavailable, fall back to writing a candidate note via POST /v1/candidates/{id}/activity_feed with the scheduled date as structured text.
- On reschedule: the agent must PATCH the existing scheduled_interview_id rather than creating a duplicate record. Store the greenhouse.scheduled_interview_id from the initial booking in the workflow state for use on any subsequent reschedule run.
- Retry logic: on a non-200 response from Greenhouse, wait 5 minutes and retry once. If the second attempt also fails, post a Slack alert to the recruiter channel with the candidate name, Greenhouse ID, and the error_code. Do not silently swallow failures.
03End-to-end data flow
// ============================================================
// INTERVIEW SCHEDULING AUTOMATION: END-TO-END DATA FLOW
// ============================================================
// TRIGGER
// Source: Greenhouse webhook (candidate stage change)
// Webhook event: 'candidate_stage_change'
// Delivery: HTTP POST to automation platform inbound webhook URL
// ─────────────────────────────────────────────────────────────
INBOUND {
candidate_id : string // e.g. 'cand_48291'
job_id : string // e.g. 'job_1024'
stage_name : string // e.g. 'Technical Interview'
candidate_email : string // e.g. 'alex.rivers@email.com'
candidate_name : string // e.g. 'Alex Rivers'
job_name : string // e.g. 'Senior Product Designer'
triggered_at : ISO8601 // webhook delivery timestamp
}
// DEDUPE CHECK
// Platform run-log lookup: candidate_id + stage_name + triggered_at (10-min window)
IF duplicate -> DROP and log to error_log table, exit.
// ============================================================
// AGENT 1: AVAILABILITY MATCHING AGENT
// ============================================================
// Step 1: Notion lookup
// Query: Notion DB filtered by Stage Name == stage_name
notion.panel_config {
interviewers[] // Google account emails of assigned panel
duration_minutes // e.g. 60
interview_format // e.g. 'Video - Google Meet'
focus_area[] // per interviewer, maps to interviewers[] by index
booking_window_days // e.g. 7
slack_user_id[] // per interviewer, pre-mapped in Notion
greenhouse_user_id[] // per interviewer, pre-mapped in Notion
cv_link // Greenhouse candidate profile URL (constructed)
}
// Step 2: Google Calendar API query (per interviewer)
// Method: POST /calendar/v3/freeBusy
// Scope: calendar.readonly
// Window: now+1hr to now+booking_window_days (business hours only, 09:00-17:30 local)
calendar.freebusy_response {
calendars[interviewer_email].busy[] // { start: ISO8601, end: ISO8601 }
}
// Step 3: Slot computation
// Enumerate 15-min-buffered windows where all interviewers are free
// Select top 3 non-overlapping windows of duration_minutes
slot_list.ranked[] { // max 3 items
slot_id // internal UUID (generated)
start_utc // ISO8601
end_utc // ISO8601
interviewers[] // emails of confirmed-free participants
calendly_scope_window // { start: start_utc, end: end_utc } for Calendly scoping
}
// GUARD: if slot_list.ranked.length < 1
// -> POST Slack alert to recruiter channel: 'No slots found for {candidate_name}'
// -> Log to error_log table with candidate_id, stage_name, triggered_at
// -> EXIT workflow, recruiter handles manually
// ── HANDOFF: Availability Matching Agent -> Scheduling and Comms Agent ───────���──
// Payload passed internally within the orchestration workflow
handoff_payload_1 {
slot_list.ranked[]
candidate.email
candidate.name
candidate.greenhouse_id
candidate.job_name
interviewers[] // { email, name, slack_user_id, greenhouse_user_id, focus_area, cv_link }
interview.format
interview.duration_minutes
interview.stage_id // Greenhouse stage ID
}
// ============================================================
// AGENT 2: SCHEDULING AND COMMS AGENT
// ============================================================
// Step 4: Generate scoped Calendly booking link
// Calendly API: POST /scheduling_links
// Scope the event type to the top-ranked slot window (calendly_scope_window)
calendly.booking_link {
uri // unique booking URL for this candidate
event_type_uuid // pre-configured Calendly event type UUID
owner // primary interviewer Calendly account
}
// Step 5: Send booking link email via Gmail
// From: scheduling@[YourCompany.com]
// To: candidate.email
// Template fields: candidate.name, calendly.booking_link.uri, interview.format,
// interview.duration_minutes, job_name
gmail.booking_email_sent = true
// Step 6: Wait for Calendly webhook (invitee.created)
// Timeout: 48 hours
// On timeout -> Slack alert to recruiter, log exception, EXIT
// GUARD: Calendly sends invitee.canceled before invitee.created (edge case)
// -> Route to manual exception path
calendly.booking_confirmed {
event.uri
event.start_time // ISO8601
event.end_time // ISO8601
invitee.email // candidate email (verify matches candidate.email)
invitee.name
event_type_uuid
}
// Step 7: Create Google Calendar event
// Method: POST /calendar/v3/calendars/primary/events
// Scope: calendar.events
google_calendar.event {
event_id // returned by API, store for reschedule PATCH
summary // '{candidate.name} - {job_name} - {stage_name}'
start.dateTime // calendly.booking_confirmed.event.start_time
end.dateTime // calendly.booking_confirmed.event.end_time
attendees[] // candidate.email + all interviewer emails
conferenceData.entryPoints // Google Meet link (auto-generated)
description // interview format, focus areas, prep notes
reminders.useDefault // false (automation manages reminders)
}
// Step 8: Send Slack briefing (per interviewer)
// Method: POST /chat.postMessage
// Scopes: chat:write, users:read
// One DM per interviewer, not a shared channel
FOR each interviewer in interviewers[] {
slack.dm {
channel // interviewer.slack_user_id
text // 'Hi {interviewer.name}, interview confirmed with {candidate.name}...'
blocks[] // candidate name, role, date/time, cv_link, focus_area
}
slack.briefing_sent[] // append interviewer.slack_user_id
}
// Step 9: Schedule day-before reminder via Gmail
// Fire at: calendly.booking_confirmed.event.start_time - 20 hours
// To: candidate.email
// From: scheduling@[YourCompany.com]
// Template fields: candidate.name, google_calendar.event.conferenceData (Meet link),
// event.start_time, interview.format, prep notes
gmail.reminder_scheduled_at // = event.start_time - 20hr (stored in workflow state)
// RESCHEDULE PATH (Calendly fires invitee.canceled then invitee.created)
IF calendly.reschedule detected {
PATCH google_calendar.event (event_id) {
start.dateTime // new start
end.dateTime // new end
}
CANCEL gmail.reminder_scheduled_at (old)
RE-SEND Slack briefings with updated datetime
RESCHEDULE gmail.reminder_scheduled_at (new start - 20hr)
IF new slot outside original calendly_scope_window -> route to manual exception
}
// ── HANDOFF: Scheduling and Comms Agent -> ATS Update Agent ──────────────────────
handoff_payload_2 {
candidate.greenhouse_id
confirmed_slot.start_utc // = calendly.booking_confirmed.event.start_time
confirmed_slot.end_utc // = calendly.booking_confirmed.event.end_time
google_calendar.event_id
interviewers[] // { greenhouse_user_id, name }
interview.format
interview.stage_id
}
// ============================================================
// AGENT 3: ATS UPDATE AGENT
// ============================================================
// Step 10: Write confirmed interview to Greenhouse
// Method: POST /v1/scheduled_interviews
// Auth: Greenhouse API key with 'Manage Interviews' permission
greenhouse.scheduled_interview_request {
application_id // resolved from candidate.greenhouse_id
interview_id // Greenhouse interview ID for this stage
interviewers[] // { user_id: greenhouse_user_id }
start // { date_time: confirmed_slot.start_utc }
end // { date_time: confirmed_slot.end_utc }
location // google_calendar.event.conferenceData link
}
greenhouse.scheduled_interview_response {
id // scheduled_interview_id (store for reschedule PATCH)
status // 'scheduled'
}
// On success: log to run_log table { candidate_id, scheduled_interview_id, logged_at }
// On failure: retry after 5 min, then Slack alert to recruiter if still failing
// ============================================================
// WORKFLOW COMPLETE
// Recruiter remaining task: handle manual exception flags only
// ============================================================04Recommended build stack
More documents for this process
Every document generated for Interview Scheduling.