Recruiting & Interview Scheduling

This guide shows how to build an AI recruiting tool that screens candidates and schedules interviews with hiring managers — without back-and-forth emails.

The problem

Scheduling interviews is painful. Your AI recruiter screens candidates, but then the handoff breaks — the recruiter needs to check three interviewers’ calendars, find overlapping times, email the candidate options, wait for a reply, confirm with the interviewer, and send a calendar invite. Each step adds days to your hiring pipeline.

With Slotflow, your AI recruiter books the interview directly after screening — one API call, no human coordinator needed.

Architecture

Candidate applies → AI Recruiter screens
→ Not qualified → Rejection email
→ Qualified → Find interviewer with availability
→ Present time options to candidate
→ Book interview via Slotflow
→ Webhook → Send calendar invites

Setup

1. Create your interviewers

const interviewers = [
{ name: "Lisa Wang", role: "engineering_manager", timezone: "America/San_Francisco" },
{ name: "David Park", role: "senior_engineer", timezone: "America/New_York" },
{ name: "Anna Schmidt", role: "hiring_manager", timezone: "Europe/Berlin" },
];
const interviewerIds = [];
for (const person of interviewers) {
const res = await fetch("https://api.slotflow.dev/v1/humans", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(person),
});
const human = await res.json();
interviewerIds.push({ id: human.id, role: human.role });
}

2. Set interview availability

Interviewers typically have dedicated interview windows, not their entire workday:

// Lisa: interviews on Tuesday and Thursday afternoons
await setAvailability(lisaId, {
working_days: [2, 4], // Tuesday, Thursday only
work_start: "13:00",
work_end: "17:00",
meeting_durations: [45, 60], // 45-min screening, 60-min technical
});
// David: interviews Monday through Wednesday mornings
await setAvailability(davidId, {
working_days: [1, 2, 3],
work_start: "09:00",
work_end: "12:00",
meeting_durations: [45, 60],
});

3. Block holidays and sprints

// Block Lisa during the company offsite
await fetch(`https://api.slotflow.dev/v1/humans/${lisaId}/overrides`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "block",
title: "Company offsite",
all_day: true,
start_date: "2026-04-06",
end_date: "2026-04-10",
}),
});
// Block David during bi-weekly sprint planning
await fetch(`https://api.slotflow.dev/v1/humans/${davidId}/overrides`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "block",
title: "Sprint planning",
all_day: false,
start_date: "2026-03-01",
start_time: "09:00",
end_time: "10:30",
recurrence: { freq: "weekly", interval: 2, days: [1] }, // Bi-weekly Monday
}),
});

When you need to find the first available interviewer for a candidate:

async function findInterviewSlots({ interviewerIds, dateFrom, dateTo, duration }) {
const headers = { "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}` };
const results = [];
for (const interviewerId of interviewerIds) {
const res = await fetch(
`https://api.slotflow.dev/v1/humans/${interviewerId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=${duration}`,
{ headers }
);
const data = await res.json();
if (data.slots.length > 0) {
results.push({
interviewerId,
slots: data.slots,
timezone: data.timezone,
});
}
}
// Sort by earliest available slot
results.sort((a, b) =>
new Date(a.slots[0].starts_at).getTime() - new Date(b.slots[0].starts_at).getTime()
);
return results;
}

Booking the interview

After the candidate selects a time:

async function bookInterview({
interviewerId,
slot,
duration,
candidateName,
candidateEmail,
jobId,
applicationId,
interviewStage,
}) {
const res = await fetch("https://api.slotflow.dev/v1/bookings", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
human_id: interviewerId,
starts_at: slot.starts_at,
duration,
attendee_name: candidateName,
attendee_email: candidateEmail,
metadata: {
job_id: jobId,
application_id: applicationId,
stage: interviewStage, // "phone_screen", "technical", "onsite"
scheduled_by: "ai_recruiter",
},
}),
});
if (res.status === 201) {
return { success: true, booking: await res.json() };
}
if (res.status === 409) {
return { success: false, reason: "slot_taken" };
}
return { success: false, reason: (await res.json()).error.message };
}

Webhook handler

Use the webhook to send calendar invites and update your ATS:

app.post("/webhooks/slotflow", async (req, res) => {
const { event, data } = req.body;
if (event === "booking.confirmed") {
const { metadata } = data;
// Update ATS (Applicant Tracking System)
await ats.updateApplication(metadata.application_id, {
status: `${metadata.stage}_scheduled`,
interview_time: data.starts_at,
interviewer_id: data.human_id,
});
// Send calendar invite to both parties
await sendCalendarInvite({
candidateEmail: data.attendee_email,
interviewerEmail: await getInterviewerEmail(data.human_id),
startTime: data.starts_at,
endTime: data.ends_at,
title: `Interview: ${data.attendee_name}${metadata.stage}`,
});
}
if (event === "booking.cancelled") {
await ats.updateApplication(data.metadata.application_id, {
status: "needs_rescheduling",
});
}
res.sendStatus(200);
});

Interview pipeline stages

Use metadata to track which interview stage each booking represents:

// Phone screen: 45 minutes with recruiter
await bookInterview({
interviewerId: recruiterId,
duration: 45,
interviewStage: "phone_screen",
// ...
});
// Technical interview: 60 minutes with engineer
await bookInterview({
interviewerId: engineerId,
duration: 60,
interviewStage: "technical",
// ...
});

Your webhook handler and ATS integration can track the candidate’s progress through each stage using the metadata.stage field.

Key takeaways

  • Dedicated interview windows — set availability to specific days/hours, not the interviewer’s entire workday
  • Block recurring meetings — sprint planning, team syncs, and all-hands that interviewers can’t move
  • Route by interview stage — phone screens to recruiters, technical rounds to engineers, culture fits to managers
  • Use metadata for ATS integration — pass job_id, application_id, and stage through to your webhook handler
  • Search multiple interviewers — find the first available person to minimize time-to-interview