Optimize Schedules With the Nylas Calendar API

Optimize Schedules With the Nylas Calendar API

14 min read
Tags:

If you’re building an app that helps end-users become more productive, building streamlined scheduling features is a must. Users that frequently find their days packed with meetings often have trouble finding time to focus on tasks that require deeper attention because their day can easily be fragmented into short blocks of time that don’t allow them to fully engage with their tasks. 

Studies have shown that regular blocks of one to three hours can have major benefits to a person’s ability to work quickly and productively, so it’s important to make sure the awesome scheduling features you add to your app don’t interfere with other aspects of the user’s life.

In this post, you’ll learn how to use the Nylas Calendar API to build a calendar integration that:

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. The examples also assume that the connected user account is a Google Calendar account, but most of the content here works for 100% of all calendar providers.

Prepare Your Development Environment

Before we get started optimizing our user’s schedule, there is some setup we need to do and some helper functions we need to build to get the necessary data. The first step is to install the Nylas Python SDK by heading over to the terminal and running the command pip install nylas. Then, create a file named optimize_schedule.py, open it in your favorite text editor, and let’s get started!

Import Required Libraries

The first thing we need to do is import a few libraries that will help us implement scheduling functionality. The time and datetime packages will be used to handle dates and times, sys for exit conditions, os for environment variables, at last, but certainly not least, the the APIClient class from the Nylas Python SDK will make it easy to connect to Nylas.

import time # time
import datetime #dates
import sys # exit conditions
import os # environment variables
from nylas import APIClient # Nylas API client

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.

# Create a Nylas client and initialize it with the appriopriate API credentials
def initialize_nylas():
    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,
    )
    return nylas

How to Calculate User Availability

The next set of functions we’ll write will give us information about the user’s availability for the current work week.

Determine the User’s Work Schedule for the Week.

Our goal is to optimize the user’s schedule for an entire week, so we need to get a list of time objects that represent the epoch timestamp for the start and end of each work day this week. These times will be the outer bands that all of the scheduling happens within. 

The next function:

  1. accepts datetime objects that represent the start and end of a single work day, 
  2. uses them to create similar datetime objects for all days in this work week (Monday – Friday), 
  3. converts the datetime objects into strings representing the epoch timestamp for the datetime, and 
  4. returns an array that contains the epoch timestamps for each day of the week.
# Get a list of tuples that represent the user's work schedule for the week.
def working_hours(start_time, end_time):
    today = datetime.date.today()

    time_list = []
    
    for day in range(0, 5):
        # https://stackoverflow.com/a/1622052
        weekday = today + datetime.timedelta(days=-today.weekday()) + datetime.timedelta(days=+day)

        weekday_and_start_time = datetime.datetime.combine(weekday, start_time.time())
        weekday_and_start_time_unix = weekday_and_start_time.strftime("%s")

        weekday_and_end_time = datetime.datetime.combine(weekday, end_time.time())
        weekday_and_end_time_unix = weekday_and_end_time.strftime("%s")

        time_list.append((weekday_and_start_time_unix,weekday_and_end_time_unix))
    return time_list

Here is an example of what this function returns:

[
 [
  "1592231400", # Monday
  "1592262000"
 ],
 [
  "1592317800", # Tuesday
  "1592348400"
 ],
   ... # etc
]

Get the User’s Primary Calendar

Now we need to pick the user calendar to optimize. This could be done in a variety of ways, but for this example we’ll choose the calendar that has the user’s email address as its name because this is how Google Calendar represents the user’s primary calendar. This next function accepts the Nylas client object we created and our user’s email address, and searches for the calendar that has this email address as its name using the Calendar endpoint. It returns the ID for this calendar so we can work with it later.

# Get the calendar_id for the user's primary calendar
def get_calendar_id(nylas, email):
    calendar_id = ""
    for calendar in nylas.calendars.all():
        # Get the primary calendar for a Google Calendar account
        if calendar["name"] == email:
            calendar_id = calendar["id"]
            break
    if calendar_id == "":
        print("Could not find a calendar for the email #{}, please provide a valid email".format(email))
        sys.exit()
    return calendar_id

Here is a JSON example of a typical calendar returned with Nylas:

{
   "id": "asgr5q35tasdfg56uy56ughtkj",
   "account_id": "gv865mng98d843jg09sw3",
   "name": "[email protected]",
   "description": null,
   "read_only": false,
   "object": "calendar"
}

Get the User’s Availability

Now, we’ll use the Nylas Calendar API Free-Busy endpoint to get a list of the times the user is busy during their schedule for the week. This endpoint accepts a start and end time, and an email address to check for availability. Our function will take the list of time slots returned by working_hours() and find all of the busy times the user has for each of the time slots.

# Make an API call to get all busy periods for each weekday during working hours
def get_all_free_busy(nylas, email, time_list):
    free_busy_week = []
    for time_pair in time_list:
        free_busy = nylas.free_busy(email, time_pair[0], time_pair[1])
        free_busy_week.append(free_busy)
    return free_busy_week

This function will return a list of Free-Busy objects for each day of the week, here’s an example of what this looks like for a single time slot:

[  
  [
    {
     "email": "[email protected]",
     "object": "free_busy",
     "time_slots": [
      {
       "end_time": 1592499300,
       "object": "time_slot",
       "start_time": 1592497800,
       "status": "busy"
      },
      {
       "end_time": 1592503200,
       "object": "time_slot",
       "start_time": 1592499600,
       "status": "busy"
      }
     ]
    }
  ]
]

We need one last minor helper function to set the stage: a function to return a human-readable date that we’ll use later.

# Returns a date in yyyy-mm-dd format for a specified time pair
def get_date_from_time_pair(time_pair):
    return datetime.datetime.fromtimestamp(int(time_pair[0])).strftime('%Y-%m-%d')

Classify How Users Spend Their Time

Now that we have access to data that represents user availability we can analyze how they are spending their time. To do so, we’ll categorize their time into one of two categories:

  • Uninterrupted – Blocks of free time that are 90 minutes or longer. This represents enough time for users to fully focus on their work without the interruption of meetings.
  • Fragmented – Blocks of free time that are shorter than 90 minutes. This will be used to identify meetings that the user should consider rescheduling later.

Identify Uninterrupted Focus Time

Our next function accepts the list of time pairs created with the working_hours() function and the list of free-busy objects a user has for a specific day. It identifies all blocks of time within the working day where the user has more than 90 minutes of uninterrupted free time.

# Process free_busy objects to determine when there are blocks of 90+ min of free time
def get_uninterrupted(time_pair, free_busy):
    uninterrupted_time = []
    start_time = time_pair[0]
    end_time = time_pair[1]

    # If the user doesn't have any meetings for the day,
    # return the entire time slot as an uninterrupted time
    if not free_busy[0]["time_slots"]:
        uninterrupted_time.append({
            'start_time': int(start_time),
            'end_time': int(end_time),
        })
        return uninterrupted_time
    
    # Start the search from the beginning of the work day
    previous_start_of_free_time = int(start_time)
    # Iterate through each time_slot and calculate free time
    for time_slot in free_busy[0]["time_slots"]:
        free_time = time_slot["start_time"] - previous_start_of_free_time
        if free_time > 90 * 60:
            uninterrupted_time.append({
                'start_time': previous_start_of_free_time,
                'end_time': time_slot["start_time"],
                })
        previous_start_of_free_time = int(time_slot["end_time"])

    # Check the time between the last meeting and the end of the work day
    last_event_end_time = free_busy[0]["time_slots"][-1]["end_time"]
    free_time_before_eod = int(end_time) - last_event_end_time
    if free_time_before_eod > 90 * 60:
        uninterrupted_time.append({
            'start_time': last_event_end_time,
            'end_time': int(end_time),
            })
    return uninterrupted_time

This functions returns a list of dictionary objects for each day that include start and end times for each of these blocks of time that looks something like this: 

[
  [
    {'start_time': 1592577000, 'end_time': 1592592300}, 
    {'start_time': 1592595000, 'end_time': 1592604000}
  ],
  ... 
]

Identify Fragmented Time

Now we’ll do the exact same thing as the last function, except we’ll identify all blocks of free time that are less than 90 minutes.

# Process free_busy objects to determine when there are blocks of less than 90 min of free time
def get_fragmented(time_pair, free_busy):
    fragmented_time = []
    start_time = time_pair[0]
    end_time = time_pair[1]

    # If the user doesn't have any meetings for the day,
    # return an empty list
    if not free_busy[0]["time_slots"]:
        return fragmented_time

    # Start the search from the beginning of the work day
    previous_start_of_free_time = int(start_time)
    # Iterate through each time_slot and calculate free time    
    for time_slot in free_busy[0]["time_slots"]:
        free_time = time_slot["start_time"] - previous_start_of_free_time
        if free_time == 0:
            continue
        if free_time < 90 * 60:
            fragmented_time.append({
                'start_time': previous_start_of_free_time,
                'end_time': time_slot["start_time"],
                })
        previous_start_of_free_time = int(time_slot["end_time"])

    # Check the time between the last meeting and the end of the work day
    last_event_end_time = free_busy[0]["time_slots"][-1]["end_time"]
    free_time_before_eod = int(end_time) - last_event_end_time
    if free_time_before_eod < 90 * 60 and free_time_before_eod != 0:
        fragmented_time.append({
            'start_time': last_event_end_time,
            'end_time': int(end_time), })

    return fragmented_time

Optimize User Meetings

Now we can use our list of fragmented time slots to identify meetings that might need to be moved to better optimize the user’s day. To determine this, we’ll look for any events on the user’s calendar that are immediately before or after the fragmented time slot. Our next function searches the user’s primary calendar for all events that fit this criteria.

# Identify events that are immediatele before or after a fragmented time slot
def get_unoptimized(calendar_id, time_pair, fragmented_time):
    start_time = int(time_pair[0])
    end_time = int(time_pair[1])
    unoptimized_events = []

    for time_slot in fragmented_time:
        if time_slot["start_time"] != start_time:
            #use a 5 minute buffer to find the event
            end_after = time_slot["start_time"] - 5 * 60
            end_before = time_slot["start_time"] + 5 * 60
            # Query the Nylas Events Endpoint for events within the specified time
            event = nylas.events.where(
                    calendar_id=calendar_id,
                    ends_after=end_after,
                    ends_before=end_before).first()
            if event and event["id"] not in unoptimized_events:
                unoptimized_events.append(event["id"])

        if time_slot["end_time"] != end_time:
            # Use a 5 minute buffer to find events
            start_after = time_slot["end_time"] - 5 * 60
            start_before = time_slot["end_time"] + 5 * 60
            event = nylas.events.where(
                    calendar_id=calendar_id,
                    starts_after=start_after,
                    starts_before=start_before).first()
            if event and event["id"] not in unoptimized_events:
                unoptimized_events.append(event["id"])
    return unoptimized_events

Calculate Total Uninterrupted and Fragmented Time

The last helper function we need is one to tabulate the total amount of time that is uninterrupted or fragmented for a specified day.

# Take a list of time and calculate the total duration of all the time pairs
def calculate_total_time(time_list):
    total = 0
    for time_pair in time_list:
        total += (time_pair["end_time"] - time_pair["start_time"])
    return int(total / 60)

Build Streamlined Scheduling Functionality

Now, it’s time to write our main function that will bring together all of the code we’ve written into an experience that can help our user better optimize their schedule. 

The main function does the following:

  1. Initialize the Nylas client object, use it to retrieve the user’s email address, and determine the ID for the user’s primary calendar.
  2. Assign a start time and end time for the work day and create a list of epoch time stamps for every day this week that represent the specific start and end times for each day.
  3. Get the user’s availability for each day of the current working week.
  4. For each individual day:
    1. Identify all uninterrupted blocks of time
    2. Identify all fragmented blocks of time
    3. Get a list of meetings that can potentially be optimized
  5. Print a JSON representation of this data to the command line for the entire work week.
if __name__ == "__main__":
    nylas = initialize_nylas()
    email = nylas.account.email_address
    calendar_id = get_calendar_id(nylas, email)

    # Define start / end times for workday
    start_time = datetime.datetime.strptime("9:30AM", '%I:%M%p')
    end_time = datetime.datetime.strptime("6:00PM", '%I:%M%p')

    # Get time stamps for all work days in the week
    time_list = working_hours(start_time, end_time)
    # Get a user's free-busy info for the work week
    free_busy_week = get_all_free_busy(nylas, email, time_list)

    response = []
    # For each day...
    for i in range(0, 5):
        date_object = {}
        # Get a human-readable date format
        date_object["date"] = get_date_from_time_pair(time_list[i])

        # Find the user's uninterrupted focus time and fragmented time
        uninterrupted_time = get_uninterrupted(time_list[i], free_busy_week[i])
        fragmented_time = get_fragmented(time_list[i], free_busy_week[i])
        
        # Get the unoptimized meetings for the user's week.
        unoptimized_meetings = get_unoptimized(calendar_id, time_list[i], fragmented_time)

        # Construct the final JSON response.
        date_object["uninterrupted"] = {
            "minutes": calculate_total_time(uninterrupted_time),
            "time_slots": uninterrupted_time,
        }
        date_object["fragmented"] = {
            "minutes": calculate_total_time(fragmented_time),
            "time_slots": fragmented_time,
        }
        date_object["unoptimized_meetings"] = unoptimized_meetings
        response.append(date_object)

    print(json.dumps(response, indent=1))

Run this script from the terminal and you should see an output that looks something like this.

$ python3 optimize_schedule.py
[
    {
        'date': '2020-05-25',
        'uninterrupted': {
                'minutes': 240,
                'time_slots': [{
                        'start_time': 1590433200,
                        'end_time': 1590440400
                }, {
                        'start_time': 1590447600,
                        'end_time': 1590454800
                }]
        },
        'fragmented': {
                'minutes': 90,
                'time_slots': [{
                        'start_time': 1590426000,
                        'end_time': 1590429600
                }, {
                        'start_time': 1590442200,
                        'end_time': 1590444000
                }]
        },
        'unoptimized_meetings': [
          '3lyt5ouyleouu5dhp210isscs', 
          'e8c2a31d08j04kxfvhj4vve81', 
          'egq4t604kw1yinkxbki1swvqj'
        ]
    }, 
    ...
]

Now we can use this data to give the user the ability to do the following:

  • Reschedule unoptimized meetings to a better time.
  • Schedule new meetings into fragmented time slots when possible to avoid decreasing the user’s uninterrupted focus time

Build Features Users Will Love With Nylas

The Nylas Communications 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.

Related resources

How to Solve Webhook Integration Challenges with PubSub Notification Channel

Key Takeaways This article addresses the challenges of webhook integration and introduces the PubSub Notification…

How to Send Emails Using an API

Key Takeaways This post will provide a complete walkthrough for integrating an email API focused…

How to build a CRM in 3 sprints with Nylas

What is a CRM? CRM stands for Customer Relationship Management, and it’s basically a way…