Automating Calendar Event Notifications with Google Apps Script and Slack: A Step-by-Step Guide

Google Calendar and Slack are indispensable tools for managing schedules and team communications. By integrating these two, you can automate sending notifications about calendar events to a Slack channel, ensuring everyone stays informed. This article will guide you through creating a Calendar Events Notification App using Google Apps Script and Slack.

## Table of Contents

1. **Introduction**
2. **Setting Up Your Environment**
   - Required Tools and Accounts
   - Obtain the Group Calendar ID
   - Create a Slack Webhook
3. **Creating a Google Apps Script Project and Write Scripts**
   - Writing the Script
   - Sending Notifications to Slack
   - Posting Daily Events
   - Checking for New or Updated Events
   - Bonus Functions for Debugging
4. **Setting up Apps Script Triggers**
   - Trigger for Daily Event Notifications
   - Trigger for Calendar Updates
5. **Conclusion**
   - Resources


## Setting Up Your Environment
### Required Tools and Accounts

| | Usage |
|-|-|
| Google Account  |  To access Google Calendar and Google Apps Script. |
| Slack Workspace  |  To send notifications to a Slack channel. |

### Step 1. Obtain the Group Calendar ID
Before integrating with Google Calendar, you need the ID of the calendar you want to monitor.

1. Go to Google Calendar
1. Go to Settings and `Setings for my calendars` section
1. Select the calendar you want to monitor
1. Find and copy the `Calendar ID`


### Step 2. Create a Slack Webhook
To send notifications to Slack, you need to create an Incoming Webhook and obtain a webhook URL to a specific slack channel to which you intend to send notifications.

1. Go to Slack API: https://api.slack.com/apps
1. Create a New App from scratch
1. Set Up Incoming Webhooks:
   1. In the "Features" section on the left, select "Incoming Webhooks".
   1. Turn on "Activate Incoming Webhooks".
1. Add a New Webhook to Workspace:
   1. Scroll down and click "Add New Webhook to Workspace".
   1. Select the channel where you want to post the notifications.
1. After adding, you will see a webhook URL. Copy it

:::note warn
Note: If you encounter the message `[you app name] にはインストールするボットユーザーがありません`, it indicates that the app requires a bot user. Ensure that the bot user is enabled.
:::


## Create a Google Apps Script Project and Write a script
Now, we have all the ingredients we need such as Calendar ID and Slack Webhook URL we're good to create new Apps Script project. 

### My app will have 3 functionalities:
1. Sending Notifications to Slack
2. Posting Daily Events
3. Checking&Sending for New or Updated Events of the current day
4. Bonus functions useful for debugging

### Step 1: Create a Google Apps Script Project
Go to https://script.google.com/home and Create a New Apps Script Project. 


### Step 2: Writing the Script
Before writing scripts, let's first insert the obtained Google Calendar ID and Slack webhook URL

```js
const SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'; // Replace with your Slack webhook URL
const CALENDAR_ID = 'your_calendar_id@group.calendar.google.com'; // Replace with your calendar ID
```

#### Sending Notifications to Slack
This `sendSlackNotification` function sends a message to a Slack channel using a webhook URL.

```js
// 🎯 Sending Notifications to Slack
function sendSlackNotification(message) {
  const payload = {
    text: message,
    link_names: 1, // ✅Tips: Ensures that @mentions are recognized by Slack
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
  };

  UrlFetchApp.fetch(SLACK_WEBHOOK_URL, options);
}
```
:::note info
If you want to add @ mentions in your message (channel, here, etc.) then add `link_names: 1` to your payload. Otherwise, your mentions will be recognized as text.  
:::


#### Posting Daily Events
This `postDailyEvents` function posts events that day's events to the Slack channel at a specific time every day. Read inline code comments for more details
<details><summary>postDailyEvents()</summary>

```js
// 🎯 Post daily events 
function postDailyEvents() {
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const tomorrow = new Date(today);
  tomorrow.setDate(tomorrow.getDate() + 1);

  const events = CalendarApp.getCalendarById(CALENDAR_ID).getEvents(today, tomorrow);

  let messageHeader =  ':spiral_calendar_pad: *今日の予定を共有します:*\n\n'
  let message = messageHeader;
  let postedEvents = [];

  events.forEach(event => {
    const eventId = event.getId();
    const startTime = event.getStartTime().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    const endTime = event.getEndTime().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });

    message += `▸ *${event.getTitle()}*: \n     時間：(${startTime} ~ ${endTime})\n`;

    // Add event details to postedEvents array
    postedEvents.push({ id: eventId, startTime, endTime });
  });

  // Send a message if there are events
  if (message !== messageHeader) {
    message += `\n 本日もよろしくお願いします！`
    sendSlackNotification(message);
  } else {
    Logger.log('There are no events for today.');
  }

  // Store the posted event details to avoid duplicate posting of the same event by checkNewCalendarEvents()
  const properties = PropertiesService.getScriptProperties();
  properties.setProperty('postedEvents', JSON.stringify(postedEvents));
}
```
</details>

This is the result message in Slack:

:::note info
<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/97a865e7-52bc-631e-7bc1-54252b7abf41.png">
:::


#### Checking for New or Updated Events
This `checkNewCalendarEvents` function checks for new or updated events and sends notifications accordingly. More specifically, it does two things: 
1. It posts a message to a Slack channel when a new event is added to the calendar for today. 
2. It posts a message to a Slack channel when today's events start/end time gets updated. 

Read inline code comments for more details. Please don't mind too many code comments they're there to explain the logic of the code for beginners. 

<details><summary>checkNewCalendarEvents()</summary>

```js
// 1. 🎯  Post newly created events for today 
// 2. 🎯  Post today's events when their start/end time gets updated
function checkNewCalendarEvents() {
   // Access the script properties service to store and retrieve persistent key-value pairs.
  const properties = PropertiesService.getScriptProperties(); 
  // Retrieve the posted events from the previous runs or initialize an empty array if none are found.
  const postedEvents = JSON.parse(properties.getProperty('postedEvents') || '[]');  
  // Get the last checked time or use the current time if not found.
  const lastChecked = new Date(properties.getProperty('lastChecked') || new Date().getTime());  

  const now = new Date();  
  // Create a new Date object representing the start of today.
  const startOfToday = new Date(now); 
  // Set the time of the startOfToday to midnight (00:00:00). 
  startOfToday.setHours(0, 0, 0, 0);  
  // Create a new Date object representing the start of tomorrow.
  const startOfTomorrow = new Date(startOfToday);  
  // Move the date to the next day.
  startOfTomorrow.setDate(startOfTomorrow.getDate() + 1);  

  // Retrieve events from the calendar for today.
  const events = CalendarApp.getCalendarById(CALENDAR_ID).getEvents(startOfToday, startOfTomorrow); 

  // Initialize an array to store the current events.
  let newPostedEvents = [];  
  
  events.forEach(event => {  
    const eventId = event.getId();  
    // Get the start time of the event in a readable format. Remove milliseconds and only display hours and minutes
    const startTime = event.getStartTime().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    const endTime = event.getEndTime().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    // Check if the event already exists in the posted events.
    const existingEvent = postedEvents.find(e => e.id === eventId);  

    if (existingEvent) {  // If the event exists in the posted events:
      // Check if event start or end time has changed
      if (existingEvent.startTime !== startTime || existingEvent.endTime !== endTime) { 
        // Create a message indicating the event time has changed.
        const message = `@channel \n\n :pencil: *本日の予定の時間が変更されました:* \n  ▸ *${event.getTitle()}* \n        時間：(${startTime} ~ ${endTime})`;  
        sendSlackNotification(message); 
      }
    } else {  // If the event does not exist in the posted events:
    // Create a message indicating a new event has been added.
      const message = `@channel \n\n :new_1: *本日の新しい予定が追加されました:* \n  ▸ *${event.getTitle()}* \n        時間：(${startTime} ~ ${endTime})`;  
      sendSlackNotification(message);  
    }

    // Add the current event details to the new posted events array.
    newPostedEvents.push({ id: eventId, startTime, endTime });  
  });

  // Update the posted events property with the new posted events array to avoid duplicate posting of the same event by checkNewCalendarEvents() when the calendar updates
  properties.setProperty('postedEvents', JSON.stringify(newPostedEvents));  
  // Update the last checked time with the current time.
  properties.setProperty('lastChecked', now.getTime());  
}
```
</details>

This is the result message in Slack:

:::note info
New event:
<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/11650d57-d79c-7546-4111-249622e4abc7.png">

Updated existing event:
<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/b7022b88-5ab3-acd3-6cb5-345eb0b5b8f3.png">

:::

#### Bonus functions useful for debugging
Functions to check and clear "posted event" properties for debugging and resetting the state.

To test and debug your script, you can manually run the functions and check the logs using Logger.log(). 

<details><summary>checkProperties()</summary>

```js
// 🛠️ Check the state of posted events. 
// Use it manually to debug and verify the events that have been posted.
function checkProperties() {
  const properties = PropertiesService.getScriptProperties();
  const postedEvents = JSON.parse(properties.getProperty('postedEvents'));  

  Logger.log(`Posted events: ${postedEvents}`);
}
```
</details>

<details><summary>clearChannelProperties()</summary>

```js
// 🛠️ Clears the 'postedEvents' property from script properties.
// Use it manually to reset the state if you need to start fresh, ensuring that no previous events are posted and thus avoiding potential duplication in notifications.
function clearChannelProperties() {
  const properties = PropertiesService.getScriptProperties();
  properties.deleteProperty('postedEvents');

  Logger.log('Channel properties cleared.');
}
```
</details>


### Step 3: Setting up Apps Script Triggers
Both postDaily (to post daily events) and checkNewCalendarEvents (Checking for New or Updated Events) have to be triggered to check the calendar and send notifications to Slack. To trigger both of the functions we will need to add triggers from "Triggers" tab. 

Here you can see 2 separate triggers that I have added: 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/8ff31ead-3992-c07c-e521-fbfd428c97ad.png)


#### 1. Trigger for postDailyEvents function
We want this function to get triggered once a day, preferrable in the morning, and send all the planned events for the day. Therefore, we set-up a `Time-driven` trigger just like in the screenshot image. 
<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/93a18174-5e69-bdd9-6146-e0a0744d1cee.png">


#### 2. Trigger for checkNewCalendarEvents function
We want this function to get trigger every time calendar updates. Therefore, we set-up a separate `From calendar` trigger for it just like in the screenshot image. 
Don't forget to replace your `Calendar owner email` so that Apps Script can watch the changes in your calendar.

<img width="400" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620296/760ac2e9-fee7-00af-cad9-a48c5ed469e3.png">

## Conclusion
Integrating Google Calendar with Slack using Google Apps Script allows for automated, real-time event notifications. This setup can enhance team communication and efficiency. Customize and extend the script to fit your specific needs.


### Useful Resources:
- Checkout the Full Code on GitHub: https://github.com/OybekJP/CalendarNotifier
- https://developers.google.com/apps-script
- https://api.slack.com/apps
- https://api.slack.com/messaging/webhooks