Recruiting

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

1const interviewers = [
2 { name: "Lisa Wang", role: "engineering_manager", timezone: "America/San_Francisco" },
3 { name: "David Park", role: "senior_engineer", timezone: "America/New_York" },
4 { name: "Anna Schmidt", role: "hiring_manager", timezone: "Europe/Berlin" },
5];
6
7const interviewerIds = [];
8for (const person of interviewers) {
9 const res = await fetch("https://api.slotflow.dev/v1/humans", {
10 method: "POST",
11 headers: {
12 "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
13 "Content-Type": "application/json",
14 },
15 body: JSON.stringify(person),
16 });
17 const human = await res.json();
18 interviewerIds.push({ id: human.id, role: human.role });
19}

2. Set interview availability

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

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

3. Block holidays and sprints

1// Block Lisa during the company offsite
2await fetch(`https://api.slotflow.dev/v1/humans/${lisaId}/overrides`, {
3 method: "POST",
4 headers: {
5 "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
6 "Content-Type": "application/json",
7 },
8 body: JSON.stringify({
9 type: "block",
10 title: "Company offsite",
11 all_day: true,
12 start_date: "2026-04-06",
13 end_date: "2026-04-10",
14 }),
15});
16
17// Block David during bi-weekly sprint planning
18await fetch(`https://api.slotflow.dev/v1/humans/${davidId}/overrides`, {
19 method: "POST",
20 headers: {
21 "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
22 "Content-Type": "application/json",
23 },
24 body: JSON.stringify({
25 type: "block",
26 title: "Sprint planning",
27 all_day: false,
28 start_date: "2026-03-01",
29 start_time: "09:00",
30 end_time: "10:30",
31 recurrence: { freq: "weekly", interval: 2, days: [1] }, // Bi-weekly Monday
32 }),
33});

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

1async function findInterviewSlots({ interviewerIds, dateFrom, dateTo, duration }) {
2 const headers = { "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}` };
3
4 const results = [];
5
6 for (const interviewerId of interviewerIds) {
7 const res = await fetch(
8 `https://api.slotflow.dev/v1/humans/${interviewerId}/slots?date_from=${dateFrom}&date_to=${dateTo}&duration=${duration}`,
9 { headers }
10 );
11 const data = await res.json();
12
13 if (data.slots.length > 0) {
14 results.push({
15 interviewerId,
16 slots: data.slots,
17 timezone: data.timezone,
18 });
19 }
20 }
21
22 // Sort by earliest available slot
23 results.sort((a, b) =>
24 new Date(a.slots[0].starts_at).getTime() - new Date(b.slots[0].starts_at).getTime()
25 );
26
27 return results;
28}

Booking the interview

After the candidate selects a time:

1async function bookInterview({
2 interviewerId,
3 slot,
4 duration,
5 candidateName,
6 candidateEmail,
7 jobId,
8 applicationId,
9 interviewStage,
10}) {
11 const res = await fetch("https://api.slotflow.dev/v1/bookings", {
12 method: "POST",
13 headers: {
14 "Authorization": `Bearer ${process.env.SLOTFLOW_API_KEY}`,
15 "Content-Type": "application/json",
16 },
17 body: JSON.stringify({
18 human_id: interviewerId,
19 starts_at: slot.starts_at,
20 duration,
21 attendee_name: candidateName,
22 attendee_email: candidateEmail,
23 metadata: {
24 job_id: jobId,
25 application_id: applicationId,
26 stage: interviewStage, // "phone_screen", "technical", "onsite"
27 scheduled_by: "ai_recruiter",
28 },
29 }),
30 });
31
32 if (res.status === 201) {
33 return { success: true, booking: await res.json() };
34 }
35
36 if (res.status === 409) {
37 return { success: false, reason: "slot_taken" };
38 }
39
40 return { success: false, reason: (await res.json()).error.message };
41}

Webhook handler

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

1app.post("/webhooks/slotflow", async (req, res) => {
2 const { event, data } = req.body;
3
4 if (event === "booking.confirmed") {
5 const { metadata } = data;
6
7 // Update ATS (Applicant Tracking System)
8 await ats.updateApplication(metadata.application_id, {
9 status: `${metadata.stage}_scheduled`,
10 interview_time: data.starts_at,
11 interviewer_id: data.human_id,
12 });
13
14 // Send calendar invite to both parties
15 await sendCalendarInvite({
16 candidateEmail: data.attendee_email,
17 interviewerEmail: await getInterviewerEmail(data.human_id),
18 startTime: data.starts_at,
19 endTime: data.ends_at,
20 title: `Interview: ${data.attendee_name} — ${metadata.stage}`,
21 });
22 }
23
24 if (event === "booking.cancelled") {
25 await ats.updateApplication(data.metadata.application_id, {
26 status: "needs_rescheduling",
27 });
28 }
29
30 res.sendStatus(200);
31});

Interview pipeline stages

Use metadata to track which interview stage each booking represents:

1// Phone screen: 45 minutes with recruiter
2await bookInterview({
3 interviewerId: recruiterId,
4 duration: 45,
5 interviewStage: "phone_screen",
6 // ...
7});
8
9// Technical interview: 60 minutes with engineer
10await bookInterview({
11 interviewerId: engineerId,
12 duration: 60,
13 interviewStage: "technical",
14 // ...
15});

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