Build Sophisticated Sequential Scheduling Features With the Nylas Calendar API

The Nylas Calendar API makes it simple to add sequential scheduling functionality to your app with 100% of calendar providers.

Ben Lloyd Pearson | July 23, 2020

If you’re building an app to help users make their schedules more productive, you’ve quite likely encountered scenarios where a user needs to sequentially schedule multiple meetings. Maybe your user is a manager who needs to schedule back to back 1:1 meetings with their direct reports, or perhaps your user is a healthcare provider who needs to schedule labs and specialist appointments as part of a single patient visit. 

Update: availability is now even easier. Since the publication of this post, we’ve released new Availability and Consecutive Availability endpoints that automatically handle the scenarios outlined here.

Want a PDF of this article?

Share it with a friend or save it for later reading.

The problem with building this functionality yourself is that you need to do a many to one comparison of schedule availability and construct schedules that fit the availability of all individuals; this quickly becomes a complex problem as you deal with availability across multiple calendars. The Nylas Calendar API comes equipped with all the tools and resources you need to get this done, so let’s get started!

In this post, you’ll learn how to

The examples in this post will demonstrate how to build this functionality with the Nylas Python SDK, but we also offer SDKs for Node.js, Java, and Ruby. Before starting, you’ll need to take 30 seconds to register your Nylas account and get your developer API keys. Specifically, you need the Client ID and Client Secret for your Nylas app, and an access token for an email account that you want to use. We’ll save these as environment variables so they’re more secure; take a look at our guide on Python environment variables to learn more.

Prepare Your Development Environment

The first step is to install the nylas package by heading over to the terminal and running the command pip install nylas. Then, create a file named sequential_schedule.py, open it in your favorite text editor, and let’s get started!

Import Libraries

We’ll start by importing a few libraries we need; os lets us access tokens that we’ve stored securely on our local environment, datetime is used to manage dates and time, and nylas provides an APIClient class that makes it easy to connect to the Nylas Email, Calendar, and Contacts APIs.

import os
import datetime
from nylas import APIClient

Initialize the Nylas Client

Now, initialize the Nylas API client object by passing the client ID and secret for your Nylas app, and an access token for an account that has been connected to Nylas. If you’re new to Nylas, sign up for a free developer account, and follow our 30 second guide to get your API keys. For the next example, we’ve stored these tokens as environment variables for better security, take a look at our guide on Python environment variables to learn more.

CLIENT_ID = os.environ['CLIENT_ID']
CLIENT_SECRET = os.environ['CLIENT_SECRET']
ACCESS_TOKEN = os.environ['ACCESS_TOKEN']

nylas = APIClient(
    CLIENT_ID,
    CLIENT_SECRET,
    ACCESS_TOKEN,
)

Define global variables

We’re also going to define some global variables to make it easier to test out this functionality. Ideally, these should be passed to the script as arguments, but we’ll include them in the script for simplicity.

  • direct_reports – A list of email addresses that the user would like to schedule sequential meetings with.
  • meeting_length – An integer representing the number of minutes we want to schedule with each person, total_time is the total amount of time it takes to schedule all meetings.
  • date_range, work_start, and work_end define the times and dates that we will search for availability.
# Global Variables
direct_reports = ["[email protected]", "[email protected]"]
meeting_length = 30
total_time = meeting_length * len(direct_reports)
date_range = "6/15/2020 - 6/19/2020"
work_start = "10:00am"
work_end = "6:00pm"

user_email = nylas.account.email_address

Use Free-Busy to Compare User Availability

The next set of functions will leverage the Free-Busy endpoint to compare availability between the primary user and the list of email addresses we provided via the direct_reports variable.

Evaluate User Availability

The first step in comparing the availability of our user and their team is to get a list of all time slots the primary user is available for the given days and times. The first function accepts an amount of time as an integer representing minutes, a datetime object for a specific date, and the email address of the user to check for availability. It uses the Nylas Free-Busy endpoint to identify all times the user is busy during the specified time.

# Accepts: an amount of time as an integer representing minutes, 
# A datetime object for a specific date, 
# And the email address of the user to check for availability.
def find_available_timeslots(total_time, date, email_address):
    start_time =  datetime.datetime.strptime(work_start, "%I:%M%p")
    end_time = datetime.datetime.strptime(work_end, "%I:%M%p")

    # Nylas uses epoch time stamps to represent time.
    start = datetime.datetime.combine(date, start_time.time()).strftime("%s")
    end = datetime.datetime.combine(date, end_time.time()).strftime("%s")

    # Use the Free-Busy endpoint to get all of the times the user is busy during the specified time.
    free_busy = nylas.free_busy(email_address, start, end)[0]
    available_time_slots = []
    start_of_time_slot = int(start)
    end_of_day = int(end)

    # If the user has no events, their entire day is free.
    if len(free_busy['time_slots']) == 0:
        available_time_slots.append({
            'start_time': start_of_time_slot,
            'end_time': end_of_day,
            })
        return available_time_slots

    for time_slot in free_busy['time_slots']:
        # If the gap between two busy periods is longer than
        # the total required time for the sequential events...
        if time_slot["start_time"] - start_of_time_slot > total_time:
            # Add this slot to the user's list of availability
            available_time_slots.append({
                'start_time': start_of_time_slot,
                'end_time': time_slot["start_time"],
                })
        start_of_time_slot = time_slot["end_time"]
    
    # Check the last free period of the user's day
    # to see if it is long enough for the sequential events
    if end_of_day - free_busy['time_slots'][-1]["end_time"] > total_time:
        available_time_slots.append({
            'start_time': time_slot["end_time"],
            'end_time': end_of_day,
            })
    # Return all available time slots long enough for all meetings.
    return available_time_slots

This function returns a list of dictionary objects containing the start and end time of all times the user is available to schedule the sequential meetings, represented as epoch timestamps in seconds.

[
  {
    'start_time': 1595430000, 
    'end_time': 1595448000
  }, 
  {
    'start_time': 1595449800, 
    'end_time': 1595451600
  },
  ... 
]

Compare Availability Between Accounts

The next function will accept the list of available time slots for our user and match them to the availability of each direct report. It will represent this availability as a list of time slots that both the primary user and the provided email address are available, and an overall average of the availability match represented as a percentage. We’ll use this number to help schedule the sequential meetings or to notify the primary user of which email addresses have the least amount of availability in the case that the script is unable to schedule times that work for everyone.

# Accepts a list of time slots from find_available_timeslots(), 
# and a list of email addresses to match availability
def eval_timeslots(time_slots, emails):
    evaluated_timeslots = []
    # For each available time slot the user has...
    for time_slot in time_slots:
        # Create a key for a percentage representation of availability match between users
        time_slot["user_match"] = []
        time_slot_total = time_slot["end_time"] - time_slot["start_time"]
        # For each of the email addresses to match availability with...
        for email in emails:
            # Use the Nylas Free-Busy endpoint to return availability info
            free_busy = nylas.free_busy(email, time_slot["start_time"], time_slot["end_time"])[0]
            # If nothing is returned, it means the user is 100% available
            if len(free_busy["time_slots"]) == 0:
                time_slot["user_match"].append({
                    'user': email,
                    'match': 100,
                    })
            # Otherwise, calculate the total amount of time the person is busy
            else:
                busy_time = 0
                for busy_time_slot in free_busy["time_slots"]:
                    busy_time = busy_time_slot['end_time'] - busy_time_slot['start_time']
                # Show the availability of the email address as a percent of total
                time_slot["user_match"].append({
                    'user': email,
                    'match': 100 - int((busy_time / time_slot_total) * 100),
                    })
        # Calculate the average availability of all the provided email addresses
        total = 0
        for match in time_slot["user_match"]:
            total += match["match"]
        average_match = int(total / len(time_slot["user_match"]))
        time_slot["average_match"] = average_match
        evaluated_timeslots.append(time_slot)

    return evaluated_timeslots

This function will return a list of dictionary objects for each possible timeslot that shows the average availability for each individual email, as well as the overall average availability for all email addresses.

[
  {
    "start_time": 1595430000,
    "end_time": 1595458800,
    "user_match": [
      {
        "user": "[email protected]",
        "match": 100
      },
      {
        "user": "[email protected]",
        "match": 50
      }
    ],
    "average_match": 75
  }
]

Summarize Account Availability

The last availability function will calculate the average availability for each email address across all of the time slots that are all users are available in. This information will be used in the situation where the script is unable to find time to sequentially schedule all meetings on the primary user’s calendar. It can be used to determine which email address has the most scheduling conflicts which would allow the user to individually schedule separate meetings when there are a lot of schedule conflicts.

# Accepts: A list returned by evaluated_timeslots()
def match_availability(evaluated_timeslots):
    user_availability = {}
    # For all of the time slots we evaluated earlier...
    for time_slot in evaluated_timeslots:
        for user_match in time_slot["user_match"]:
            if user_match["user"] not in user_availability.keys():
                user_availability[user_match["user"]] = []
            user_availability[user_match["user"]].append(user_match["match"])
    # Return a list of users and their average availability
    availability = []
    for user,matches in user_availability.items():
        availability.append({
            'user': user,
            'match': sum(matches) / len(matches)
            })
    return availability

This function will return a list of dictionaries that indicate the average availability for each email address.

[
  {
    "user": "[email protected]",
    "match": 65
  },
  {
    "user": "[email protected]",
    "match": 22
  }
]

Plan Sequential Schedules

Now it’s time to make a plan for the sequential events. The next function will take the list of evaluated time slots we created earlier, divide each time slot into chunks of time that match the length of the meetings we need to schedule, and identify start and end times that make a sequential schedule that includes meetings with all provided email addresses.
# Accepts: A list of time slots that we'll attempt to sequentially schedule the meetings 
def plan_meetings(time_slots):
    if len(time_slots) > 0:
        # Choose the time slot with the highest average availability first
        # This ensures we start with the most likely possibility.
        highest_time_slot = {"average_match": 0}
        for time_slot in time_slots:
            if time_slot["average_match"] > highest_time_slot["average_match"]:
                highest_time_slot = time_slot
        # Sort the users by availability from least to most.
        # Starting with least available user makes it more likely to find time for everyone
        sorted_users = sorted(highest_time_slot["user_match"], key=lambda item: item["match"])

        # Generate a list of available time slots that are divided into equal lengths 
        available_time_slots = []
        for i in range(
                highest_time_slot["start_time"],
                highest_time_slot["end_time"] - meeting_length * 60,
                meeting_length * 60
                ):
            available_time_slots.append({'start_time': i, 'end_time': i + meeting_length * 60})

        planned_time_slots = []
        planned_users = []
        # For each email address in our sorted list...
        for user in sorted_users:
            # For each time slot the user is available
            for time_slot in available_time_slots:
                # Check the availability for the email address
                free_busy = nylas.free_busy(user["user"], time_slot["start_time"], time_slot["end_time"])[0]
                # Schedule the email address in the first available slot
                if len(free_busy["time_slots"]) == 0:
                    planned_time_slots.append({
                        'start_time': time_slot["start_time"],
                        'end_time': time_slot["end_time"],
                        'user': user["user"],
                        })
                    planned_users.append(user["user"])
                    # Make the slot unavailable after scheduling an event
                    available_time_slots.remove(time_slot)
                    break
            if user["user"] not in planned_users:
                print("could not find available time for {}".format(user["user"]))
                return []
        return planned_time_slots

    else:
        return []

This function returns a list of possible meeting times for each email address that creates a sequential schedule.

[
  {
    "start_time": 100000,
    "end_time": 400000,
    "user_schedule": [
      {
        "user": "[email protected]",
        "start_time": 100000,
        "end_time": 200000
      },
      {
        "user": "[email protected]",
        "start_time": 200000
        "end_time": 300000
      }
    ]
  },
  ...
]

Schedule the Meetings

The last function we need for our script is one to use the Nylas Events endpoint to create the events and send email invites to the relevant email addresses. 
# Accepts a list of time slots generated by plan_meetings()
def schedule_meetings(planned_time_slots):
    for planned_time_slot in planned_time_slots:
        event = nylas.events.create()
        calendars = nylas.calendars.all()
        for calendar in calendars:
            if calendar["name"] == user_email:
                event.calendar_id = calendar["id"]
        event.title = "1 on 1 with {}".format(user_email)
        event.description = "Let's catch up on our current progress"
        event.busy = True
        event.participants = [
                {'email': user_email},
                {'email': planned_time_slot["user"]},
                ]
        event.when = {
                "start_time": planned_time_slot["start_time"],
                "end_time": planned_time_slot["end_time"],
                }
        event.save()
    return True

Build a Sequential Schedule

Now it’s time to bring all of our functions together in our main loop. The main loop parses the dates that are provided to it and executes the helper functions sequentially for each day. It then uses the first available time slot to schedule sequential events with all of the provided email addresses and sends them email invites. If you’re adding this functionality to your app, you could present your user with an option to choose which time slot the sequential schedule is created in.

if __name__ == "__main__":
    start_date,end_date = date_range.split(" - ")
    start =  datetime.datetime.strptime(start_date, "%m/%d/%Y")
    end =  datetime.datetime.strptime(end_date, "%m/%d/%Y")
    dates = [start + datetime.timedelta(days=x) for x in range(0, (end-start).days + 1)]

    time_slots = {}
    planned_time_slots = []
    for date in dates:
        # Find all times the primary user is available
        available_timeslots = find_available_timeslots(total_time, date, user_email)
        # Identify the time slots that the provided email addresses are also available
        evaluated_timeslots = eval_timeslots(available_timeslots, direct_reports)
        # Identify possible sequential schedules
        planned_time_slots.append(plan_meetings(evaluated_timeslots))
        # Calculate overall email address availability
        user_availability = match_availability(evaluated_timeslots)
    # Automatically schedule the meetings for the first available time slot.
    for planned_time_slot in planned_time_slots:
        if len(planned_time_slot > 0):
            schedule_meetings(planned_time_slot)
            break
        else:
          print("Unable to schedule meetings with all email addresses.")
          # Show availability info so the user can schedule meetings individually
          print(user_availability)

Run this script from the terminal and it will automatically schedule our user’s meetings!

$ python3 sequential_schedule.py

Build More Features Users Will Love With Nylas

The Nylas Platform is the easiest way to integrate with 100% of calendar providers in the world. Here’s a few resources to teach you about what else is possible with Nylas.

Ben Lloyd Pearson

Ben is the Developer Advocate for Nylas. He is a triathlete, musician, avid gamer, and loves to seek out the best breakfast tacos in Austin, Texas.