Skip to main content
An agent schedules interviews for a recruiting team. It reads the times candidates offer in their replies, matches them against the interviewers’ calendars, books the interviews, confirms each one in the candidate’s thread and posts the schedule to the hiring channel. A cron job can start it each morning while a role is open. A job this small needs no VM or sandbox. It runs in a lightweight terminal inside one FastAPI or Express server:
  • Files: /work, mounted in memory for the agent’s scratch files. To keep them, mount a disk folder, an S3 bucket or another VFS there instead.
  • Python: python3, in-process on Monty.
  • CLIs: himalaya reads and sends mail, gws calls Google Calendar and slack posts to Slack, with credentials the server holds, so the agent never sees one.
The commands below are what the agent runs in its terminal. A company is hiring a senior backend engineer. Six candidates have replied to the recruiting inbox with two times each for the week of 16 November 2026, and two engineers, Dana Lee and Omar Haddad, run every interview. All times are in UTC.

Installation

himalaya needs the email extra in Python and three mail libraries in Node:

Server

This is the server from the Overview with one addition: it registers himalaya, gws and slack, each with one account. To run this page you need three accounts: a mailbox the agent reads and sends from over IMAP and SMTP, a Google account with Calendar access that can see both interviewers’ free and busy times, and a Slack bot that is a member of the hiring channel. Email, Google and Slack show how to get each credential. The server reads them from environment variables, and Service Credentials shows how to keep them in a secret store instead:
EMAIL_SENT_FOLDER names the folder the account keeps sent mail in: Sent on most servers, [Gmail]/Sent Mail on Gmail and Sent Items on Exchange. himalaya saves each sent message there. The ports, the TLS switch and the two API addresses default to the services’ own. Set them only for a self-hosted mail server or a proxy. A terminal has its own environment, not the server’s, so the agent uses the three accounts without seeing their credentials.
app.py
The output below comes from sample data, so yours will differ.

Usage

Create a terminal, and define run, which sends commands to it and prints what they print. It needs jq:
The commands between SH and SH in the blocks below run in the terminal on the server, not in the shell that calls run.

Read the inbox

The agent counts the messages in the inbox, then searches for the candidates’ replies by subject, skipping any it has already answered. It saves each reply to a file under /work/replies, named by its message ID, so later steps read the files instead of the mailbox:
The inbox holds 8 messages. Six are replies; the other two are a note from Dana and a notice from a job board. --page-size 0 returns every match instead of a page of 25, up to the newest 200 messages, the account’s max_messages.

Find the offered times

The recruiting team asked each candidate for two times, one per line, written as 2026-11-16 10:00. A short Python script takes each candidate’s name and address from the sender in the search results, using the address as the name when there is none, and the times from the reply’s body. It saves them to /work/candidates.json and prints a table:
The first column is the message ID, which the agent uses later to reply in the same thread. The candidates are in the order they replied.

Check the calendars

Google Calendar’s free/busy query returns the times each person is busy, without the details of their meetings. The agent asks for both interviewers for the whole week and saves the answer to a file. Monty has no network, so the Python script reads that file. For each candidate, in the order they replied, it picks the first offered hour when neither interviewer is busy. A booked hour counts as busy for the candidates after it:
Google gives busy times in UTC, such as 2026-11-16T10:00:00Z. The script keeps their first 19 characters, the date and the time to the second, so it can compare them with the candidates’ times. Maya and Lucas both offered Monday at 10:00. Maya replied first and gets it, so Lucas gets his second time. Each of Priya’s times clashes with at least one interviewer, so the agent leaves her for a person.

Book the interviews

jq turns each booked row of the plan into a Calendar event, one per line, with the candidate and both interviewers as guests. The agent creates the events on the recruiting calendar, then lists that calendar to check them:

Confirm and tell the team

The agent replies to each booked candidate in their own thread and, once the reply is sent, flags their message as answered. It then checks the sent folder for the five replies. mailbox list gives that folder the role sent. It writes the schedule to a file, posts it to the hiring channel, #hiring-backend, by its Slack channel ID, and reads the post back:
The first five lines are the recipients of the replies in the sent folder, newest first. The rest is the post in #hiring-backend. himalaya message reply without --send only prints the reply, which is a safe way for an agent to show a draft before it sends. The answered flag is kept in the mailbox, so the next morning’s run skips the five booked candidates and plans only new replies and Priya’s. A reply that fails to send prints its error and leaves the message unflagged, so the next run tries that candidate again. The round ends with what the agent can report:
  • Five interviews are booked, each on the recruiting calendar with the candidate, Dana and Omar as guests, and each candidate has a confirmation in their thread.
  • Lucas Ortega’s first time was taken by Maya Chen, so he has his second, Monday at 15:00.
  • Priya Raman cannot be scheduled: Dana or Omar is busy at each of her times. A person needs to ask her for more times or move a meeting.
  • #hiring-backend has the full schedule.
To set up these and the other bundled CLIs, see CLIs.