Skip to content

Instantly share code, notes, and snippets.

@Accudio
Last active August 21, 2025 13:32
Show Gist options
  • Save Accudio/4ff04216b99aeed4061d1fad177961f2 to your computer and use it in GitHub Desktop.
Save Accudio/4ff04216b99aeed4061d1fad177961f2 to your computer and use it in GitHub Desktop.
Custom Google Calendar → Slack Status sync using a Google Apps Script and the Slack API

Custom Google Calendar → Slack Status sync

Features

  • Custom emojis and text
  • Write custom functions to set the status how you'd like depending on event properties like colour, title, type
  • Doesn't overwrite a user-defined status
  • Only updates when a status changes
  • Accurate to within 1 minute

Set up

  1. Create a new Google Apps Script project at https://script.google.com
  2. Add the "Calendar v3" service by clicking '+' next to "Services" on the left
  3. Paste the code from Code.gs
  4. Create a Slack app and go to "OAuth & Permissions"
    1. Add User Token Scopes of users.profile:read and users.profile:write
    2. Install to Series Eight
    3. Copy User OAuth Token
  5. Populate constants:
    1. CALENDAR_ID — probably your email address, you can find from Calendar Settings in Google Calendar
    2. SLACK_TOKEN — User token from above
    3. SLACK_USER — Slack User ID, go to your profile in Slack, kebab menu, then "Copy Member ID"
  6. Customise statuses and statusConditions (see below)
  7. Add a trigger within Apps Script
    1. "Triggers" on the left (the clock icon)
    2. "Add Trigger"
    3. "Choose which function to run" — main
    4. "Select type of time-based trigger" — Minutes timer
    5. "Select minute interval" — Every minute
    6. Save
  8. Confirm it works and use the "Executions" log to see execution status and any logging

Statuses and Status Conditions

Statuses is an object that defines your Slack statuses. The key is a custom key that is returned by status conditions. The value is an array of emoji and text. The emoji should be in Slack's format of :emoji_name:. This is easily found by copying+pasting from the Slack message field.

Status conditions is an array of predicate functions that run sequentially. The functions are passed a single parameter of an array of all events running at that time. The first function to return defines the status, so you can choose a priority order if you have multiple events at once. Consider using .find() with a condition. If a function returns null, it moved onto the next one in the array.

Examples

// clear the status when there are no events
(events) => events.length === 0 ? 'none' : null

// based on the color set within Google Calendar
(events) => {
    return events.find(event => event.colorId === '11') ? 'client-call' : null
}

// based on event name
(events) => {
    return events.find(event => event.summary.toLowerCase() === 'team social') ? 'social' : null
}

You get the full event object from Google Calendar so you can do loads with it. The easiest way to find what properties you can use for events is to log your events:

const statusConditions = [
    (events) => Logger.log(events)
]

Advanced

Manual running

You can manually trigger a function within the script editor from the top of the code window. In the dropdown pick the main function and then click Run and it will force run the script and show the output. Helpful for debugging.

You can also force a cache clear by running the clearCache function in the same way.

Params

cacheEvents — to avoid fetching unnecessarily fetching events from Google Calendar every minute we cache them for cacheEvents number of seconds. This is 10 minutes by default but can be changed if needed.

debug — default false, this adds additional logging, will set the status every minute instead of only when the status changes and disables the cache.

const CALENDAR_ID = "<CALENDAR_ID>";
const SLACK_TOKEN = '<SLACK_USER_TOKEN>'
const SLACK_USER = '<SLACK_USER_ID>'
/**
* Key passed from event filter, array including Slack Emoji and Status Text
* 'key': [ 'emoji', 'text' ]
*/
const statuses = {
'none': ['', ''],
'client-call': [':spiral_calendar_pad:', 'Client meeting'],
'internal-call': [':telephone_receiver:', 'Internal call'],
'ooo-lunch': [':taco:', 'Lunch'],
'ooo-swim': [':swimmer:', 'Out for a bit'],
'ooo-travel': [':steam_locomotive:', 'Travelling'],
'focus': [':no_entry:', 'Focusing'],
}
/**
* Array of predicates in priority order that may return a status.
* These will run top to bottom, and the first one to return dictates the status from above.
* Return `null` to move onto the next one
*/
const statusConditions = [
// no calendar events
(events) => events.length === 0 ? 'none' : null,
// client call
(events) => {
return events.find(event => event.colorId === '11') ? 'client-call' : null
},
// internal call
(events) => {
return events.find(event => event.colorId === '2') ? 'internal-call' : null
},
// out of office — travel
(events) => {
return events.find(event => event.eventType === 'outOfOffice' && event.summary.toLowerCase().includes('travel'))
? 'ooo-travel'
: null
},
// out of office — swim
(events) => {
return events.find(event => event.eventType === 'outOfOffice' && event.summary.toLowerCase().includes('swim'))
? 'ooo-swim'
: null
},
// out of office — lunch
(events) => {
return events.find(event => event.eventType === 'outOfOffice' && event.summary.toLowerCase().includes('lunch'))
? 'ooo-lunch'
: null
},
// qa / launch or deep work
(events) => {
return events.find(event => event.colorId === '6') || events.find(event => event.colorId === '3') ? 'focus' : null
},
// default to none
() => 'none'
]
const cacheEvents = 600 // seconds
const debug = false
eval(UrlFetchApp.fetch('https://cdn.jsdelivr.net/npm/[email protected]/build/global/luxon.min.js').getContentText());
let DateTime = luxon.DateTime;
const DTnow = DateTime.now().startOf('minute').plus({ minutes: 1 });
function clearCache() {
const cache = CacheService.getUserCache();
cache.remove('prev-status')
cache.remove('events')
}
function main() {
const cache = CacheService.getUserCache();
const events = getEvents(cache);
if (debug) Logger.log(events)
const status = getStatus(events)
const prevStatus = cache.get('prev-status')
let hasChanged = status !== prevStatus
cache.put('prev-status', status)
if (!hasChanged && !debug) {
Logger.log(`No change to status ${status}`)
return
}
const slackStatus = statuses[status]
if (!slackStatus) {
Logger.log(`Missing slackStatus for ${status}`)
return
}
const currentStatus = getSlackStatus()
const maybeMatchingStatus = Object.values(statuses).find(val => JSON.stringify(val) === JSON.stringify(currentStatus))
if (!maybeMatchingStatus) {
Logger.log('Current Slack status not ours, skipping')
if (debug) Logger.log(currentStatus)
return
}
updateSlackStatus(status)
}
function getEvents(cache) {
let events
const cached = cache.get('events')
if (cached) {
events = JSON.parse(cached)
Logger.log('fetched events from cache')
} else {
const response = Calendar.Events.list(CALENDAR_ID, {
timeMin: DTnow.toISO(),
timeMax: DTnow.plus({ seconds: cacheEvents + 60 }).toISO(),
showDeleted: false,
singleEvents: true,
orderBy: 'startTime'
});
events = response.items;
cache.put('events', JSON.stringify(events), cacheEvents)
Logger.log('fetched events remotely')
}
// filter out events that aren't currently running
events = events.filter(event => {
const endTime = DateTime.fromISO(event.end.dateTime).setZone(event.end.timeZone)
const startTime = DateTime.fromISO(event.start.dateTime).setZone(event.start.timeZone)
return startTime < DTnow && endTime > DTnow
})
if (debug) Logger.log(events)
return events
}
function getStatus(events) {
for (const condition of statusConditions) {
const result = condition(events)
if (result) return result
}
}
function getSlackStatus() {
const response = UrlFetchApp.fetch(`https://slack.com/api/users.profile.get?user=${SLACK_USER}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${SLACK_TOKEN}`
}
});
const json = JSON.parse(response)
if (debug) Logger.log(`getSlackStatus: ${JSON.stringify(json)}`)
if (json.ok) {
return [json.profile.status_emoji, json.profile.status_text]
}
return ['', '']
}
function updateSlackStatus(status) {
const slackStatus = statuses[status]
const response = UrlFetchApp.fetch('https://slack.com/api/users.profile.set', {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Authorization': `Bearer ${SLACK_TOKEN}`
},
payload: JSON.stringify({
user: SLACK_USER,
profile: {
status_emoji: slackStatus[0],
status_text: slackStatus[1],
status_expiration: 0
}
})
});
const json = JSON.parse(response)
if (debug) Logger.log(`updateSlackStatus: ${JSON.stringify(json)}`)
if (json.ok) {
Logger.log(`Successfully updated status to ${status}`)
} else {
Logger.log(`Slack error: ${JSON.stringify(json)}`)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment