> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.slotflow.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.slotflow.dev/_mcp/server.

# Recruiting & Interview Scheduling

> Build an AI recruiting tool that schedules interviews with hiring managers.

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

```javascript
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:

```javascript
// 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

```javascript
// 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
  }),
});
```

## Multi-interviewer slot search

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

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
// 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