Template Content
About the template
About ScriptRunner Connect
What is ScriptRunner Connect?
Can I try it out for free?
Yes. ScriptRunner Connect comes with a forever free tier.
Can I customize the integration logic?
Absolutely. The main value proposition of ScriptRunner Connect is that you'll get full access to the code that is powering the integration, which means you can make any changes to the the integration logic yourself.
Can I change the integration to communicate with additional apps?
Yes. Since ScriptRunner Connect specializes in enabling complex integrations, you can easily change the integration logic to connect to as many additional apps as you need, no limitations.
What if I don't feel comfortable making changes to the code?
First you can try out our AI assistant which can help you understand what the code does, and also help you make changes to the code. Alternatively you can hire our professionals to make the changes you need or build new integrations from scratch.
Do I have to host it myself?
No. ScriptRunner Connect is a fully managed SaaS (Software-as-a-Service) product.
What about security?
ScriptRunner Connect is ISO 27001 and SOC 2 certified. Learn more about our security.
This integration periodically extracts employee holiday bookings from BambooHR and posts them on a Slack channel.
Parameters
and add the Slack channel name in SLACK_CHANNEL
. Then enter the BambooHR employee ids in TEAM_EMPLOYEE_IDS
.Integration
tab.Scheduled Triggers
and configure the schedule for this script.The script will run on the schedule configured. No other action is required.
import Slack from "./api/slack";
import BambooHR from "./api/generic";
import dayjs, { Dayjs } from "dayjs";
import duration from "dayjs/plugin/duration";
import advancedFormat from "dayjs/plugin/advancedFormat";
// Add extensions to DayJS
dayjs.extend(duration);
dayjs.extend(advancedFormat);
const DELIMITER = "\nโข ";
export default async function (event: any, context: Context): Promise<void> {
const { SLACK_CHANNEL, TEAM_EMPLOYEE_IDS } = getEnvVars(context);
if (!SLACK_CHANNEL) {
throw new Error('SLACK_CHANNEL environment variable is not configured');
}
if (!TEAM_EMPLOYEE_IDS || !TEAM_EMPLOYEE_IDS.length) {
throw new Error('TEAM_EMPLOYEE_IDS environment variable is not configured. At least one employee id must be added');
}
const today = dayjs();
// Get holiday requests for this day
const initialHolidays = await fetchHolidays(today);
// Filter the holidays to only include the employees that are part of the team
const holidaysForThisTeam = initialHolidays.filter((holiday) =>
TEAM_EMPLOYEE_IDS.includes(`${holiday.employeeId}`)
);
// If employees book holidays over subsequent days across multiple requests, this will contain the true end date.
const projectedHolidays: Holiday[] = [];
for (const holiday of holidaysForThisTeam) {
const endDate = await getHolidayEndDate(holiday);
projectedHolidays.push({ ...holiday, end: endDate });
}
const sortedHolidays = (holidays: Holiday[]) => {
return holidays.sort((a, b) => (a.end.isBefore(b.end) ? -1 : 1));
};
const messages = sortedHolidays(projectedHolidays).map((holiday) =>
formatHoliday(holiday, today)
);
const holidayMessage =
messages.length < 1
? ":calendar: No holidays today.\nThis message will not self-destruct, but it should..."
: `:calendar: *Holidays today*: ${toSlackMessage(messages)}`;
console.log("Holiday message:", holidayMessage);
await Slack.Chat.postMessage({
body: {
channel: SLACK_CHANNEL,
username: "Holidaytron 4000",
icon_emoji: ":beach_with_umbrella:",
text: holidayMessage,
},
});
}
function getNextWorkingDay(date: Dayjs): Dayjs {
let daysToAdd = 1;
const dayIndex = date.day();
if (dayIndex === 5) {
daysToAdd = 3;
} else if (dayIndex === 6) {
daysToAdd = 2;
}
return date.add(daysToAdd, "day");
}
async function fetchHolidays(date: Dayjs): Promise<Holiday[]> {
const todayString = date.format("YYYY-MM-DD");
const result = await BambooHR.fetch(`v1/time_off/whos_out/?start=${todayString}&end=${todayString}`, {
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
});
const rawHolidays: RawHoliday[] = await result.json();
const holidays: Holiday[] = rawHolidays.map((holiday) => (
{ ...holiday, start: dayjs(holiday.start), end: dayjs(holiday.end) }
));
return holidays;
}
async function getHolidayEndDate(holiday: Holiday): Promise<Dayjs> {
console.log(`Finding holiday end date for employee ${holiday.employeeId}`);
let testedHoliday: Holiday | null = holiday;
let endDate: Dayjs;
do {
await sleep(200);
endDate = testedHoliday.end;
const nextWorkingDay = getNextWorkingDay(testedHoliday.end);
const result = await fetchHolidays(nextWorkingDay);
testedHoliday = result.filter((h) => h.employeeId === holiday.employeeId)[0] ?? null;
if (testedHoliday != null) {
console.log(`Found a subsequent holiday occurrence for employee ${holiday.employeeId}`);
}
} while (testedHoliday != null);
return endDate;
}
const sleep = (duration: number) => new Promise((resolve) => setTimeout(resolve, duration));
const toSlackMessage = (messages: string[]) => DELIMITER + messages.join(DELIMITER);
function formatHoliday(holiday: Holiday, today: Dayjs): string {
const dateBack = getNextWorkingDay(holiday.end);
const dateBackFormatted = dateBack.format("ddd Do MMM");
const dateDescription = getRelativeDateDescription(today, dateBack);
const returnDateDescription = dateDescription
? `${dateDescription} (${dateBackFormatted})`
: dateBackFormatted;
return `${holiday.name} - back ${returnDateDescription}`;
}
function getRelativeDateDescription(today: Dayjs, date: Dayjs): string | undefined {
today = today.startOf("day");
if (date.isAfter(today.add(13, "day"))) {
return undefined;
}
if (date.isSame(today, "date")) {
return "today";
}
if (date.isSame(today.add(1, "day"), "date")) {
return "tomorrow";
}
if (date.isBefore(today.add(7, "day"))) {
return `on ${date.format("dddd")}`;
}
const oneWeekToday = today.add(7, "day");
if (date.isSame(oneWeekToday, "day")) {
return "a week from today";
}
return `${date.format("dddd")} next week`;
}
function getEnvVars(context: Context) {
return context.environment.vars as EnvVars;
}
interface BaseHoliday {
id: number;
type: string;
employeeId: number;
name: string;
}
interface RawHoliday extends BaseHoliday {
start: string;
end: string;
}
interface Holiday extends BaseHoliday {
start: Dayjs;
end: Dayjs;
}
interface EnvVars {
SLACK_CHANNEL: string;
TEAM_EMPLOYEE_IDS: string[];
}