Build an interview-scheduling coordinator
Slot Northwind candidates into available interview windows from a Sheet of preferences.
Published Jul 28, 2025
Scheduling interviews at Northwind eats a recruiter’s day. A candidate sends three slots that work, the interviewer’s calendar has shifted since the email was drafted, and the back-and-forth begins. Multiply that by a dozen open roles and a single hire’s coordination cost is measured in hours, not minutes.
This script turns scheduling into a sweep. The recruiter pastes each candidate’s preferred slots into a sheet, the script picks the first slot the interviewer is actually free for, books the 45-minute interview with both parties invited, and writes the chosen time back to the row. The next run skips anyone already scheduled.
What you’ll need
- An
Interviewssheet with the columnscandidate,email,interviewer,preferredSlots, andscheduled. The recruiter fills the first four;scheduledis left blank for the script. - The
preferredSlotscell holds one or more ISO-style timestamps separated by a pipe, e.g.2025-07-30T10:00|2025-07-30T14:00|2025-07-31T11:00. The pipe avoids the comma collisions you get with CSV cells. - The script runs under an account that can see the interviewer’s calendar — typically the recruiter’s account, since interviewers’ calendars are usually shared within the company.
The script
// Sheet that holds the interview queue.
const INTERVIEWS_SHEET = '1abcInterviewsId';
// How long each interview lasts, in minutes.
const INTERVIEW_MINUTES = 45;
/**
* Reads the Interviews sheet, picks the first preferred slot the
* interviewer is free for, books a 45-minute calendar event with
* both parties as guests, and stamps the chosen time back into the row.
*/
function scheduleInterviews() {
const sheet = SpreadsheetApp.openById(INTERVIEWS_SHEET).getSheets()[0];
const values = sheet.getDataRange().getValues();
const [h, ...rows] = values;
const col = Object.fromEntries(h.map((k, i) => [k, i]));
const cal = CalendarApp.getDefaultCalendar();
rows.forEach((r, i) => {
// 1. Skip anyone already booked — the row is its own ledger.
if (r[col.scheduled]) return;
if (!r[col.candidate] || !r[col.email]) return;
// 2. Parse the pipe-separated slots. Drop anything that does not
// parse to a real Date so a typo cannot crash the run.
const slots = String(r[col.preferredSlots])
.split('|')
.map((s) => new Date(s.trim()))
.filter((d) => !isNaN(d));
// 3. Try each slot in order. The first one the interviewer is free
// for wins — preferences are listed in priority order.
for (const slot of slots) {
const end = new Date(slot.getTime() + INTERVIEW_MINUTES * 60_000);
// getEvents returns anything that overlaps, so a non-empty array
// means the interviewer is busy at that time.
if (cal.getEvents(slot, end).length > 0) continue;
// 4. Book it. Both candidate and interviewer get the invite.
cal.createEvent(`Interview: ${r[col.candidate]}`, slot, end, {
guests: `${r[col.email]},${r[col.interviewer]}`,
sendInvites: true,
});
// 5. Stamp the chosen slot back into the row so we never re-book.
values[i + 1][col.scheduled] = slot;
break;
}
});
// 6. One write at the end is cheaper than per-row updates.
sheet.getDataRange().setValues(values);
}
How it works
scheduleInterviewsreads the whole sheet at once and builds acollookup so the rest of the code readsr[col.email]instead ofr[1].- For every row it checks the
scheduledcell. If a time is already there the row is done — the cell is the row’s record of having been booked. - It splits
preferredSlotson|, turns each piece into aDate, and drops anything that fails to parse. A bad timestamp in one row will not stop the others. - It walks the slots in order and asks Calendar whether any events overlap.
getEvents(start, end)returns the conflicts; an empty array means free. - The first free slot wins. The script creates a 45-minute event with both the candidate and the interviewer as guests, and Calendar sends the invites automatically.
- It writes the chosen slot back into the row’s
scheduledcell and moves on. After the loop, onesetValuescall flushes the whole sheet.
Example run
Say the Interviews sheet looks like this before a run, where Priya is the
hiring manager and her 10:00 slot is already booked:
| candidate | interviewer | preferredSlots | scheduled | |
|---|---|---|---|---|
| Sam Lee | [email protected] | [email protected] | 2025-07-30T10:00|2025-07-30T14:00 | |
| Alex Carr | [email protected] | [email protected] | 2025-07-31T11:00 | |
| Jordan Mei | [email protected] | [email protected] | 2025-07-30T09:00 | (already booked) |
After scheduleInterviews runs, two new events sit on Priya’s calendar and
the sheet reads:
| candidate | interviewer | preferredSlots | scheduled | |
|---|---|---|---|---|
| Sam Lee | [email protected] | [email protected] | 2025-07-30T10:00|2025-07-30T14:00 | 2025-07-30 14:00 |
| Alex Carr | [email protected] | [email protected] | 2025-07-31T11:00 | 2025-07-31 11:00 |
| Jordan Mei | [email protected] | [email protected] | 2025-07-30T09:00 | (already booked) |
Sam’s first preference clashed, so the script fell through to 14:00. Alex only offered one slot and it was free. Jordan was already done.
Trigger it
Run it every couple of hours during recruiting season so candidates get prompt invites:
- In the Apps Script editor, open Triggers (clock icon).
- Add a trigger for
scheduleInterviews, event source Time-driven, type Hours timer, every 2 hours. - Approve the authorisation prompt the first time.
Or run it on demand — paste new candidates in, click Run, and check the sheet a moment later.
Watch out for
- The script only checks the interviewer’s calendar. If you want to check the candidate’s too, you cannot — but you can trust their preferences, and Calendar will tell them the moment a conflict appears.
- A candidate with no free slot will have a blank
scheduledcell after the run. Sort by that column and you have your follow-up list. - 45 minutes is hardcoded as
INTERVIEW_MINUTES. If different roles need different lengths, add aminutescolumn to the sheet and read it instead. sendInvites: truemeans each booking fires an email immediately. For a quiet “draft mode” run, set it tofalse, review the events, and resend invites once you are happy.- Timestamps in the sheet are read in the spreadsheet’s timezone. Set the sheet’s timezone (File → Settings) to match where the interviews happen, or you will book some uncomfortable 3am slots.
Related
Schedule personal habits and routines
Block recurring habits on Awadesh's calendar — gym, walks, deep-work mornings.
Updated Nov 5, 2025
Sync birthdays and anniversaries to Calendar
Populate recurring personal dates from a Sheet — the Northwind team rituals calendar.
Updated Nov 1, 2025
Generate recurring events with custom exceptions
Handle complex recurrence rules in code — every Tuesday except UK bank holidays.
Updated Oct 28, 2025
Auto-reschedule low-priority conflicts
Move flexible Northwind events around fixed ones — focus blocks bend, client calls don't.
Updated Oct 16, 2025
Build a contract-renewal calendar
Track Northwind's recurring-revenue renewal dates as calendar events for proactive sales.
Updated Oct 8, 2025