Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,55 @@
import { googleCalendarAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { google, calendar_v3 } from 'googleapis';
import { OAuth2Client } from 'googleapis-common';
import { googleCalendarCommon } from '../common';
export const addAttendeesToEventAction = createAction({
auth: googleCalendarAuth,
name: 'google-calendar-add-attendees',
displayName: 'Add Attendees to Event',
description: 'Add one or more person to existing event.',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
eventId: Property.ShortText({
displayName: 'Event ID',
required: true,
}),
attendees: Property.Array({
displayName: 'Attendees',
description: 'Emails of the attendees (guests)',
required: true,
}),
},
async run(context) {
const { calendar_id, eventId } = context.propsValue;
const attendeesInput = context.propsValue.attendees as string[];
const authClient = new OAuth2Client();
authClient.setCredentials(context.auth);
const calendar = google.calendar({ version: 'v3', auth: authClient });
// Note that each patch request consumes three quota units;
// prefer using a get followed by an update
const currentEvent = await calendar.events.get({
calendarId: calendar_id,
eventId: eventId,
});
const currentAttendees = currentEvent.data.attendees ?? [];
const attendeeFormattedList: calendar_v3.Schema$EventAttendee[] = [];
attendeeFormattedList.push(...currentAttendees);
attendeeFormattedList.push(...attendeesInput.map((email) => ({ email })));
const response = await calendar.events.update({
calendarId: calendar_id!,
eventId,
requestBody: {
...currentEvent.data,
attendees: attendeeFormattedList,
},
});
return response.data;
},
});

View File

@@ -0,0 +1,36 @@
import { googleCalendarAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { OAuth2Client } from 'googleapis-common';
import { google } from 'googleapis';
import { googleCalendarCommon } from '../common';
export const addCalendarToCalendarlist = createAction({
auth: googleCalendarAuth,
name: 'addCalendarToCalendarlist',
displayName: 'Add Calendar to calendarList',
description: "Adds other people's calendars to your calendarList",
props: {
id: Property.ShortText({
displayName: "Calendar Id",
description: "Find calendar id by going to calendar settings",
required: true
})
},
async run(context) {
const id = context.propsValue.id;
const authClient = new OAuth2Client();
authClient.setCredentials(context.auth);
const calendar = google.calendar({ version: 'v3', auth: authClient});
const response = await calendar.calendarList.insert({
requestBody: {
id: id
}
})
return response.data;
},
});

View File

@@ -0,0 +1,166 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { googleCalendarCommon } from '../common';
import dayjs from 'dayjs';
import { googleCalendarAuth } from '../../';
import { google } from 'googleapis';
import { OAuth2Client } from 'googleapis-common';
import { randomUUID } from 'crypto';
export const createEvent = createAction({
auth: googleCalendarAuth,
name: 'create_google_calendar_event',
description: 'Add Event',
displayName: 'Create Event',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
title: Property.ShortText({
displayName: 'Title of the event',
required: true,
}),
start_date_time: Property.DateTime({
displayName: 'Start date time of the event',
required: true,
}),
end_date_time: Property.DateTime({
displayName: 'End date time of the event',
description: "By default it'll be 30 min post start time",
required: false,
}),
location: Property.ShortText({
displayName: 'Location',
required: false,
}),
/*attachment: Property.ShortText({
displayName: 'Attachment',
description: 'URL of the file to be attached',
required: false,
}),*/
description: Property.LongText({
displayName: 'Description',
description: 'Description of the event. You can use HTML tags here.',
required: false,
}),
colorId: googleCalendarCommon.colorId,
attendees: Property.Array({
displayName: 'Attendees',
description: 'Emails of the attendees (guests)',
required: false,
}),
guests_can_modify: Property.Checkbox({
displayName: 'Guests can modify',
defaultValue: false,
required: false,
}),
guests_can_invite_others: Property.Checkbox({
displayName: 'Guests can invite others',
defaultValue: false,
required: false,
}),
guests_can_see_other_guests: Property.Checkbox({
displayName: 'Guests can see other guests',
defaultValue: false,
required: false,
}),
send_notifications: Property.StaticDropdown({
displayName: 'Send Notifications',
defaultValue: 'all',
options: {
options: [
{ label: 'Yes, to everyone', value: 'all' },
{
label: 'To non-Google Calendar guests only',
value: 'externalOnly',
},
{ label: 'To no one', value: 'none' },
],
},
required: true,
}),
create_meet_link: Property.Checkbox({
displayName: 'Create Google Meet Link',
description: 'Automatically create a Google Meet video conference link for this event',
defaultValue: false,
required: false,
}),
},
async run(configValue) {
// docs: https://developers.google.com/calendar/api/v3/reference/events/insert
const {
calendar_id: calendarId,
title: summary,
start_date_time,
end_date_time,
location,
description,
colorId,
guests_can_modify: guestsCanModify,
guests_can_invite_others: guestsCanInviteOthers,
guests_can_see_other_guests: guestsCanSeeOtherGuests,
create_meet_link: createMeetLink,
} = configValue.propsValue;
const start = {
dateTime: dayjs(start_date_time).format('YYYY-MM-DDTHH:mm:ss.sssZ'),
};
const endTime = end_date_time
? end_date_time
: dayjs(start_date_time).add(30, 'm');
const end = {
dateTime: dayjs(endTime).format('YYYY-MM-DDTHH:mm:ss.sssZ'),
};
/*const attachment = {
fileUrl: configValue.propsValue.attachment,
};*/
const attendeesArray = configValue.propsValue.attendees as string[];
const sendNotifications = configValue.propsValue.send_notifications;
const attendeesObject = [];
if (attendeesArray) {
for (const attendee of attendeesArray) {
attendeesObject.push({ email: attendee });
}
}
const authClient = new OAuth2Client();
authClient.setCredentials(configValue.auth);
const calendar = google.calendar({ version: 'v3', auth: authClient });
const requestBody: any = {
summary,
start,
end,
colorId,
//attachments: configValue.propsValue.attachment ? [attachment] : [],
location: location ?? '',
description: description ?? '',
attendees: attendeesObject,
guestsCanInviteOthers,
guestsCanModify,
guestsCanSeeOtherGuests,
};
if (createMeetLink) {
requestBody.conferenceData = {
createRequest: {
conferenceSolutionKey: {
type: 'hangoutsMeet',
},
requestId: randomUUID(),
},
};
}
const response = await calendar.events.insert({
calendarId,
sendUpdates: sendNotifications,
conferenceDataVersion: createMeetLink ? 1 : 0,
requestBody,
});
return response.data;
},
});

View File

@@ -0,0 +1,67 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
HttpRequest,
HttpMethod,
AuthenticationType,
httpClient,
} from '@activepieces/pieces-common';
import { googleCalendarCommon } from '../common';
import { googleCalendarAuth } from '../../';
export const createQuickCalendarEvent = createAction({
auth: googleCalendarAuth,
name: 'create_quick_event',
description: 'Add Quick Calendar Event',
displayName: 'Create Quick Event',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
text: Property.LongText({
displayName: 'Summary',
description: 'The text describing the event to be created',
required: true,
}),
send_updates: Property.StaticDropdown<string>({
displayName: 'Send Updates',
description:
'Guests who should receive notifications about the creation of the new event.',
required: false,
options: {
disabled: false,
options: [
{
label: 'All',
value: 'all',
},
{
label: 'External Only',
value: 'externalOnly',
},
{
label: 'none',
value: 'none',
},
],
},
}),
},
async run(configValue) {
// docs: https://developers.google.com/calendar/api/v3/reference/events/quickAdd
const calendarId = configValue.propsValue['calendar_id'];
const url = `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events/quickAdd`;
const qParams: Record<string, string> = {
text: configValue.propsValue['text'],
sendUpdates: configValue.propsValue['send_updates'] || 'none',
};
const request: HttpRequest<Record<string, unknown>> = {
method: HttpMethod.POST,
url,
body: {},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: configValue.auth.access_token,
},
queryParams: qParams,
};
return await httpClient.sendRequest(request);
},
});

View File

@@ -0,0 +1,35 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { google } from 'googleapis';
import { OAuth2Client } from 'googleapis-common';
import { googleCalendarAuth } from '../../index';
import { googleCalendarCommon } from '../common';
export const deleteEventAction = createAction({
displayName: 'Delete Event',
auth: googleCalendarAuth,
name: 'delete_event',
description: 'Deletes an event from Google Calendar.',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
eventId: Property.ShortText({
displayName: 'Event ID',
required: true,
}),
},
async run(context) {
const authClient = new OAuth2Client();
authClient.setCredentials(context.auth);
const calendarId = context.propsValue.calendar_id;
const eventId = context.propsValue.eventId;
const calendar = google.calendar({ version: 'v3', auth: authClient });
const response = await calendar.events.delete({
calendarId,
eventId,
});
return response.data;
},
});

View File

@@ -0,0 +1,98 @@
import { createAction, Property, OAuth2PropertyValue } from '@activepieces/pieces-framework';
import { HttpRequest, HttpMethod, AuthenticationType, httpClient } from '@activepieces/pieces-common';
import { googleCalendarAuth } from '../../';
import { googleCalendarCommon } from '../common';
import { getCalendars } from '../common/helper';
import dayjs from 'dayjs';
interface FreeBusyResponse {
kind: 'calendar#freeBusy';
timeMin: string;
timeMax: string;
calendars: {
[calendarId: string]: {
busy: {
start: string;
end: string;
}[];
errors?: {
domain: string;
reason: string;
}[];
};
};
}
export const findFreeBusy = createAction({
auth: googleCalendarAuth,
name: 'google_calendar_find_busy_free_periods',
displayName: 'Find Busy/Free Periods in Calendar',
description: 'Finds free/busy calendar details from Google Calendar.',
props: {
calendar_ids: Property.MultiSelectDropdown({
auth: googleCalendarAuth,
displayName: 'Calendars',
description: 'Select the calendars to check for busy periods.',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
const authProp = auth as OAuth2PropertyValue;
const calendars = await getCalendars(authProp);
return {
disabled: false,
options: calendars.map((calendar) => {
return {
label: calendar.summary,
value: calendar.id,
};
}),
};
},
}),
start_date: Property.DateTime({
displayName: 'Start Time',
description: 'The start of the time range to check.',
required: true,
}),
end_date: Property.DateTime({
displayName: 'End Time',
description: 'The end of the time range to check.',
required: true,
}),
},
async run(context) {
const { calendar_ids, start_date, end_date } = context.propsValue;
const { access_token } = context.auth;
const requestBody = {
timeMin: dayjs(start_date).toISOString(),
timeMax: dayjs(end_date).toISOString(),
items: calendar_ids.map((id) => ({ id })),
};
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${googleCalendarCommon.baseUrl}/freeBusy`,
body: requestBody,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: access_token,
},
};
const response = await httpClient.sendRequest<FreeBusyResponse>(request);
return response.body;
},
});

View File

@@ -0,0 +1,124 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
HttpRequest,
HttpMethod,
AuthenticationType,
httpClient,
} from '@activepieces/pieces-common';
import { googleCalendarCommon } from '../common';
import { googleCalendarAuth } from '../../';
import { GoogleCalendarEvent } from '../common/types';
export const getEventById = createAction({
auth: googleCalendarAuth,
name: 'google_calendar_get_event_by_id',
displayName: 'Get Event by ID',
description: 'Fetch event details by its unique ID from Google Calendar.',
props: {
calendar_id: googleCalendarCommon.calendarDropdown(),
event_id: Property.ShortText({
displayName: 'Event ID',
description:
'The unique ID of the event (e.g., "abc123def456"). You can find this in the event URL or from other calendar actions.',
required: true,
}),
max_attendees: Property.Number({
displayName: 'Max Attendees',
description:
'Maximum number of attendees to include in the response. If there are more attendees, only the participant is returned.',
required: false,
}),
time_zone: Property.ShortText({
displayName: 'Time Zone',
description:
'Time zone for the response (e.g., "America/New_York", "Europe/London"). Defaults to the calendar\'s time zone if not specified.',
required: false,
}),
},
async run(context) {
const {
calendar_id: calendarId,
event_id: eventId,
max_attendees: maxAttendees,
time_zone: timeZone,
} = context.propsValue;
const { access_token: token } = context.auth;
if (
!calendarId ||
typeof calendarId !== 'string' ||
calendarId.trim().length === 0
) {
throw new Error('Calendar ID is required');
}
if (
!eventId ||
typeof eventId !== 'string' ||
eventId.trim().length === 0
) {
throw new Error('Event ID cannot be empty');
}
if (eventId.length < 5 || eventId.length > 1024) {
throw new Error('Event ID must be between 5 and 1024 characters');
}
const queryParams: Record<string, string> = {};
if (maxAttendees !== undefined && maxAttendees > 0) {
queryParams.maxAttendees = maxAttendees.toString();
}
if (
timeZone &&
typeof timeZone === 'string' &&
timeZone.trim().length > 0
) {
queryParams.timeZone = timeZone.trim();
}
const url = `${googleCalendarCommon.baseUrl}/calendars/${encodeURIComponent(
calendarId.trim()
)}/events/${encodeURIComponent(eventId.trim())}`;
const request: HttpRequest = {
method: HttpMethod.GET,
url: url,
queryParams: queryParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: token,
},
};
try {
const response = await httpClient.sendRequest<GoogleCalendarEvent>(
request
);
return response.body;
} catch (error: any) {
if (error.response?.status === 404) {
throw new Error(
`Event with ID "${eventId}" not found in calendar "${calendarId}". Please verify the event ID and calendar selection.`
);
} else if (error.response?.status === 403) {
throw new Error(
`Access denied to event "${eventId}" in calendar "${calendarId}". Please check your permissions.`
);
} else if (error.response?.status === 400) {
throw new Error(
`Invalid request parameters. Please check the event ID format and other parameters.`
);
} else if (error.response?.status === 401) {
throw new Error(
'Authentication failed. Please reconnect your Google Calendar account.'
);
} else {
throw new Error(
`Failed to fetch event: ${error.message || 'Unknown error occurred'}`
);
}
}
},
});

View File

@@ -0,0 +1,112 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
HttpRequest,
HttpMethod,
AuthenticationType,
httpClient,
} from '@activepieces/pieces-common';
import { googleCalendarCommon } from '../common';
import dayjs from 'dayjs';
import { googleCalendarAuth } from '../../';
export const getEvents = createAction({
auth: googleCalendarAuth,
name: 'google_calendar_get_events',
description: 'Get Events',
displayName: 'Get all Events',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
event_types: Property.StaticMultiSelectDropdown({
displayName: 'Event types',
description: 'Select event types',
required: true,
defaultValue: ['default', 'focusTime', 'outOfOffice'],
options: {
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Out Of Office',
value: 'outOfOffice',
},
{
label: 'Focus Time',
value: 'focusTime',
},
{
label: 'Working Location',
value: 'workingLocation',
},
],
},
}),
search: Property.ShortText({
displayName: 'Search Term',
required: false,
}),
start_date: Property.DateTime({
displayName: 'Date from',
required: false,
}),
end_date: Property.DateTime({
displayName: 'Date to',
required: false,
}),
singleEvents: Property.Checkbox({
displayName: 'Expand Recurring Event?',
description: "Whether to expand recurring events into instances and only return single one-off events and instances of recurring events, but not the underlying recurring events themselves.",
required: true,
defaultValue: false,
}),
},
async run(configValue) {
// docs: https://developers.google.com/calendar/api/v3/reference/events/list
const {
calendar_id: calendarId,
start_date,
end_date,
search,
event_types,
singleEvents,
} = configValue.propsValue;
const { access_token: token } = configValue.auth;
const queryParams: Record<string, string> = { showDeleted: 'false' };
let url = `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events`;
if(singleEvents !== null) {
queryParams['singleEvents'] = singleEvents ? 'true' : 'false';
}
if (search) {
queryParams['q'] = `"${search}"`;
}
// date range
if (start_date) {
queryParams['timeMin'] = dayjs(start_date).format(
'YYYY-MM-DDTHH:mm:ss.sssZ'
);
}
if (start_date && end_date) {
queryParams['timeMax'] = dayjs(end_date).format(
'YYYY-MM-DDTHH:mm:ss.sssZ'
);
}
// filter by event type
if (event_types.length > 0) {
url += `?${event_types.map((type) => `eventTypes=${type}`).join('&')}`;
}
const request: HttpRequest<Record<string, unknown>> = {
method: HttpMethod.GET,
url,
queryParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token,
},
};
return await httpClient.sendRequest(request);
},
});

View File

@@ -0,0 +1,129 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { google, calendar_v3 } from 'googleapis';
import { OAuth2Client } from 'googleapis-common';
import { googleCalendarAuth } from '../../index';
import { googleCalendarCommon } from '../common';
import dayjs from 'dayjs';
export const updateEventAction = createAction({
displayName: 'Update Event',
auth: googleCalendarAuth,
name: 'update_event',
description: 'Updates an event in Google Calendar.',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
eventId: Property.ShortText({
displayName: 'Event ID',
required: true,
}),
title: Property.ShortText({
displayName: 'Title of the event',
required: false,
}),
start_date_time: Property.DateTime({
displayName: 'Start date time of the event',
required: false,
}),
end_date_time: Property.DateTime({
displayName: 'End date time of the event',
required: false,
}),
location: Property.ShortText({
displayName: 'Location',
required: false,
}),
description: Property.LongText({
displayName: 'Description',
description: 'Description of the event. You can use HTML tags here.',
required: false,
}),
colorId: googleCalendarCommon.colorId,
attendees: Property.Array({
displayName: 'Attendees',
description: 'Emails of the attendees (guests)',
required: false,
}),
guests_can_modify: Property.Checkbox({
displayName: 'Guests can modify',
defaultValue: false,
required: false,
}),
guests_can_invite_others: Property.Checkbox({
displayName: 'Guests can invite others',
defaultValue: false,
required: false,
}),
guests_can_see_other_guests: Property.Checkbox({
displayName: 'Guests can see other guests',
defaultValue: false,
required: false,
}),
},
async run(context) {
const {
calendar_id,
eventId,
title,
start_date_time,
end_date_time,
location,
description,
colorId,
guests_can_invite_others,
guests_can_modify,
guests_can_see_other_guests,
} = context.propsValue;
const attendees = context.propsValue.attendees as string[];
const authClient = new OAuth2Client();
authClient.setCredentials(context.auth);
const calendar = google.calendar({ version: 'v3', auth: authClient });
// Note that each patch request consumes three quota units;
// prefer using a get followed by an update
const currentEvent = await calendar.events.get({
calendarId: calendar_id,
eventId: eventId,
});
let attendeeFormattedList: calendar_v3.Schema$EventAttendee[] = [];
if (Array.isArray(attendees) && attendees.length > 0) {
attendeeFormattedList = attendees.map((email) => ({ email }));
} else if (
currentEvent.data.attendees &&
Array.isArray(currentEvent.data.attendees)
) {
attendeeFormattedList = currentEvent.data.attendees;
}
const response = await calendar.events.update({
calendarId: calendar_id,
eventId: eventId,
requestBody: {
summary: title ?? currentEvent.data.summary,
attendees: attendeeFormattedList,
description: description ?? currentEvent.data.description,
colorId: colorId,
location: location ?? currentEvent.data.location,
start: start_date_time
? {
dateTime: dayjs(start_date_time).format(
'YYYY-MM-DDTHH:mm:ss.sssZ'
),
}
: currentEvent.data.start,
end: end_date_time
? {
dateTime: dayjs(end_date_time).format('YYYY-MM-DDTHH:mm:ss.sssZ'),
}
: currentEvent.data.end,
guestsCanInviteOthers: guests_can_invite_others,
guestsCanModify: guests_can_modify,
guestsCanSeeOtherGuests: guests_can_see_other_guests,
},
});
return response.data;
},
});

View File

@@ -0,0 +1,228 @@
import { OAuth2PropertyValue } from '@activepieces/pieces-framework';
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { randomUUID } from 'crypto';
import { googleCalendarCommon } from '.';
import {
GoogleWatchResponse,
GoogleWatchType,
CalendarObject,
CalendarList,
GoogleCalendarEvent,
GoogleCalendarEventList,
GetColorsResponse,
} from './types';
export async function stopWatchEvent(
body: GoogleWatchResponse,
authProp: OAuth2PropertyValue
) {
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${googleCalendarCommon.baseUrl}/channels/stop`,
body: {
id: body?.id,
resourceId: body?.resourceId,
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authProp.access_token,
},
};
await httpClient.sendRequest<any>(request);
}
export async function watchEvent(
calendarId: string,
webhookUrl: string,
authProp: OAuth2PropertyValue
): Promise<GoogleWatchResponse> {
const request: HttpRequest = {
method: HttpMethod.POST,
url: `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events/watch`,
body: {
id: randomUUID(),
type: GoogleWatchType.WEBHOOK,
address: webhookUrl,
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authProp.access_token,
},
};
const { body: webhook } = await httpClient.sendRequest<GoogleWatchResponse>(
request
);
return webhook;
}
export async function getCalendars(
authProp: OAuth2PropertyValue,
minAccessRole?: 'writer'
): Promise<CalendarObject[]> {
// docs: https://developers.google.com/calendar/api/v3/reference/calendarList/list
const queryParams: Record<string, string> = {
showDeleted: 'false',
};
if (minAccessRole) {
queryParams['minAccessRole'] = minAccessRole;
}
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${googleCalendarCommon.baseUrl}/users/me/calendarList`,
queryParams: queryParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authProp.access_token,
},
};
const response = await httpClient.sendRequest<CalendarList>(request);
return response.body.items;
}
export async function getColors(
authProp: OAuth2PropertyValue
): Promise<GetColorsResponse> {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${googleCalendarCommon.baseUrl}/colors`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authProp.access_token,
},
};
const response = await httpClient.sendRequest<GetColorsResponse>(request);
return response.body;
}
export async function getEvents(
calendarId: string,
expandRecurringEvent: boolean,
authProp: OAuth2PropertyValue,
minUpdated?: Date
): Promise<GoogleCalendarEvent[]> {
// docs: https://developers.google.com/calendar/api/v3/reference/events/list
const now = new Date();
const yesterday = new Date();
yesterday.setDate(now.getDate() - 1);
const qParams: Record<string, string> = {
updatedMin: minUpdated?.toISOString() ?? yesterday.toISOString(),
maxResults: '2500', // Modified
orderBy: 'updated',
singleEvents: expandRecurringEvent ? 'true' : 'false',
showDeleted: 'true',
};
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events`,
queryParams: qParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authProp.access_token,
},
};
let eventList: GoogleCalendarEvent[] = [];
let pageToken = '';
do {
qParams['pageToken'] = pageToken;
const { body: res } = await httpClient.sendRequest<GoogleCalendarEventList>(
request
);
if (res.items.length > 0) {
eventList = [...eventList, ...res.items];
}
pageToken = res.nextPageToken;
} while (pageToken);
return eventList;
}
export async function getLatestEvent(
calendarId: string,
authProp: OAuth2PropertyValue
): Promise<GoogleCalendarEvent> {
const eventList = await getEvents(calendarId, false, authProp);
const lastUpdatedEvent = eventList.pop()!; // You can retrieve the last updated event.
return lastUpdatedEvent;
}
export async function getEventsForDropdown(
authProp: OAuth2PropertyValue,
calendarId?: string,
maxResults = 50
): Promise<{ label: string; value: string }[]> {
if (!calendarId) {
return [];
}
try {
const now = new Date();
const futureDate = new Date();
futureDate.setDate(now.getDate() + 30);
const queryParams: Record<string, string> = {
singleEvents: 'true',
orderBy: 'startTime',
timeMin: now.toISOString(),
timeMax: futureDate.toISOString(),
maxResults: maxResults.toString(),
showDeleted: 'false',
};
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${googleCalendarCommon.baseUrl}/calendars/${encodeURIComponent(
calendarId
)}/events`,
queryParams: queryParams,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: authProp.access_token,
},
};
const response = await httpClient.sendRequest<GoogleCalendarEventList>(
request
);
if (!response.body.items || response.body.items.length === 0) {
return [];
}
return response.body.items
.map((event) => {
const startTime = event.start?.dateTime || event.start?.date || '';
const startDate = startTime
? new Date(startTime).toLocaleDateString()
: '';
const startTimeFormatted = startTime
? new Date(startTime).toLocaleTimeString()
: '';
let label = event.summary || 'Untitled Event';
if (startDate) {
label += ` (${startDate}`;
if (event.start?.dateTime) {
label += ` at ${startTimeFormatted}`;
}
label += ')';
}
return {
label: label,
value: event.id || '',
};
})
.filter((item) => item.value !== '');
} catch (error) {
console.error('Error fetching events for dropdown:', error);
return [];
}
}

View File

@@ -0,0 +1,93 @@
import { OAuth2PropertyValue, Property } from '@activepieces/pieces-framework';
import { getCalendars, getColors, getEventsForDropdown } from './helper';
import { googleCalendarAuth } from '../..';
export const googleCalendarCommon = {
baseUrl: 'https://www.googleapis.com/calendar/v3',
calendarDropdown: (minAccessRole?: 'writer') => {
return Property.Dropdown<string,true,typeof googleCalendarAuth>({
auth: googleCalendarAuth,
displayName: 'Calendar',
refreshers: [],
required: true,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first',
options: [],
};
}
const authProp = auth as OAuth2PropertyValue;
const calendars = await getCalendars(authProp, minAccessRole);
return {
disabled: false,
options: calendars.map((calendar) => {
return {
label: calendar.summary,
value: calendar.id,
};
}),
};
},
});
},
eventDropdown: (required = false) => {
return Property.Dropdown<string,boolean,typeof googleCalendarAuth>({
displayName: 'Event',
refreshers: ['calendar_id'],
required: required,
auth: googleCalendarAuth,
options: async ({ auth, calendar_id }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first',
options: [],
};
}
if (!calendar_id) {
return {
disabled: true,
placeholder: 'Please select a calendar first',
options: [],
};
}
const authProp = auth as OAuth2PropertyValue;
const events = await getEventsForDropdown(
authProp,
calendar_id as string
);
return {
disabled: false,
options: events,
};
},
});
},
colorId: Property.Dropdown({
auth: googleCalendarAuth,
displayName: 'Color',
refreshers: [],
required: false,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
placeholder: 'Please connect your account first',
options: [],
};
}
const response = await getColors(auth);
return {
disabled: false,
options: Object.entries(response.event).map(([key, value]) => {
return {
label: value.background,
value: key,
};
}),
};
},
}),
};

View File

@@ -0,0 +1,246 @@
export enum GoogleWatchType {
WEBHOOK = 'web_hook',
}
export enum GoogleCalendarKind {
CALENDAR_LIST = 'calendar#calendarList',
CALENDAR_ENTRY = 'calendar#calendarListEntry',
CALENDAR_EVENT = 'calendar#event',
CALENDAR_EVENT_LIST = 'calendar#events',
EVENT_WATCH = 'api#channel',
CALENDAR_COLORS = 'calendar#colors',
}
export interface CalendarList {
kind: GoogleCalendarKind.CALENDAR_LIST;
etag: string;
nextPageToken: string;
nextSyncToken: string;
items: CalendarObject[];
}
export interface CalendarObject {
kind: GoogleCalendarKind.CALENDAR_ENTRY;
etag: string;
id: string;
summary: string;
description: string;
location: string;
timeZone: string;
summaryOverride: string;
colorId: string;
backgroundColor: string;
foregroundColor: string;
hidden: boolean;
selected: boolean;
accessRole: string;
defaultReminders: [
{
method: string;
minutes: number;
}
];
notificationSettings: {
notifications: [
{
type: string;
method: string;
}
];
};
primary: boolean;
deleted: boolean;
conferenceProperties: {
allowedConferenceSolutionTypes: string[];
};
}
export interface GoogleWatchResponse {
kind: GoogleCalendarKind.EVENT_WATCH;
id: string;
resourceId: string;
resourceUri: string;
token: string;
expiration: number;
}
interface Attendee {
id: string;
email: string;
displayName: string;
organizer: boolean;
self: boolean;
resource: boolean;
optional: boolean;
responseStatus: string;
comment: string;
additionalGuests: BigInteger;
}
export interface GoogleCalendarEvent {
kind: GoogleCalendarKind.CALENDAR_EVENT;
etag: string;
id: string;
status: string;
htmlLink: string;
created: string;
updated: string;
summary: string;
description: string;
location: string;
colorId: string;
creator: {
id: string;
email: string;
displayName: string;
self: boolean;
};
organizer: {
id: string;
email: string;
displayName: string;
self: boolean;
};
start: {
date: Date;
dateTime: Date;
timeZone: string;
};
end: {
date: Date;
dateTime: Date;
timeZone: string;
};
endTimeUnspecified: boolean;
recurrence: [string];
recurringEventId: string;
originalStartTime: {
date: Date;
dateTime: Date;
timeZone: string;
};
transparency: string;
visibility: string;
iCalUID: string;
sequence: BigInteger;
attendees: Attendee[];
attendeesOmitted: boolean;
extendedProperties: {
private: {
key: string;
};
shared: {
key: string;
};
};
hangoutLink: string;
conferenceData: {
createRequest: {
requestId: string;
conferenceSolutionKey: {
type: string;
};
status: {
statusCode: string;
};
};
entryPoints: [
{
entryPointType: string;
uri: string;
label: string;
pin: string;
accessCode: string;
meetingCode: string;
passcode: string;
password: string;
}
];
conferenceSolution: {
key: {
type: string;
};
name: string;
iconUri: string;
};
conferenceId: string;
signature: string;
notes: string;
};
gadget: {
type: string;
title: string;
link: string;
iconLink: string;
width: BigInteger;
height: BigInteger;
display: string;
preferences: {
key: string;
};
};
anyoneCanAddSelf: boolean;
guestsCanInviteOthers: boolean;
guestsCanModify: boolean;
guestsCanSeeOtherGuests: boolean;
privateCopy: boolean;
locked: boolean;
reminders: {
useDefault: boolean;
overrides: [
{
method: string;
minutes: BigInteger;
}
];
};
source: {
url: string;
title: string;
};
attachments: [
{
fileUrl: string;
title: string;
mimeType: string;
iconLink: string;
fileId: string;
}
];
eventType: string;
}
export interface GoogleCalendarEventList {
kind: GoogleCalendarKind.CALENDAR_EVENT_LIST;
etag: string;
summary: string;
description: string;
updated: number;
timeZone: string;
accessRole: string;
defaultReminders: [
{
method: string;
minutes: BigInteger;
}
];
nextPageToken: string;
nextSyncToken: string;
items: GoogleCalendarEvent[];
}
export interface GetColorsResponse {
kind: GoogleCalendarKind.CALENDAR_COLORS;
calendar: {
[s: string]: {
background: string;
foreground: string;
};
};
event: {
[s: string]: {
background: string;
foreground: string;
};
};
}

View File

@@ -0,0 +1,25 @@
---
title: 'Google Calendar'
description: ''
---
## Set up and run an app that calls a Google calendar API.
1. In the Google Cloud console, enable the Google Calendar API.
2. Click In the Google Cloud console, and go to Menu menu > APIs & Services > Credentials.
3. Click Create Credentials > OAuth client ID.
4. In the Name field, type a name for the credential. This name is only shown in the Google Cloud console.
5. Click Create. The OAuth client created screen appears, showing your new Client ID and Client secret.
6. Click OK. The newly created credential appears under OAuth 2.0 Client IDs.
---
## Triggers
TRIGGERS
---
## Actions
ACTIONS

View File

@@ -0,0 +1,139 @@
import { AppConnectionValueForAuthProperty, createTrigger, PiecePropValueSchema, Property } from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { googleCalendarCommon } from '../common';
import { getEvents } from '../common/helper';
import { GoogleCalendarEvent } from '../common/types';
import { googleCalendarAuth } from '../../';
import { DedupeStrategy, Polling, pollingHelper } from '@activepieces/pieces-common';
const polling: Polling<
AppConnectionValueForAuthProperty<typeof googleCalendarAuth>,
{ calendarId?: string; expandRecurringEvent: boolean }
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue: { calendarId, expandRecurringEvent }, lastFetchEpochMS }) => {
let minUpdated = new Date(lastFetchEpochMS);
// Google Calendar API breaks if minUpdated is too far in the past
if (lastFetchEpochMS === 0) {
const now = new Date();
const yesterday = new Date();
yesterday.setDate(now.getDate() - 7);
minUpdated = yesterday;
}
const currentValues: GoogleCalendarEvent[] =
(await getEvents(calendarId!, expandRecurringEvent, auth, minUpdated)) ?? [];
const items = currentValues.map((item) => ({
epochMilliSeconds: new Date(item.updated).getTime(),
data: item,
}));
return items;
},
};
export const calendarEventChanged = createTrigger({
// docs: https://developers.google.com/calendar/api/guides/push
auth: googleCalendarAuth,
name: 'new_or_updated_event',
displayName: 'New or Updated Event',
description: 'Triggers when an event is added or updated',
props: {
calendar_id: googleCalendarCommon.calendarDropdown(),
expandRecurringEvent: Property.Checkbox({
displayName: 'Expand Recurring Event?',
description: 'If true, the trigger will activate for every occurrence of a recurring event.',
required: true,
defaultValue: false,
}),
},
sampleData: {
kind: 'calendar#event',
etag: '3350849506974000',
id: '0nsfi5ttd2b17ac76ma2f37oi9',
htmlLink: 'https://www.google.com/calendar/event?eid=kgjb90uioj4klrgfmdsnjsjvlgkm',
summary: 'ap-event-test',
created: '2023-02-03T11:36:36.000Z',
updated: '2023-02-03T11:45:53.487Z',
description: 'Sample description',
status: 'canceled',
creator: {
email: 'test@test.com',
self: true,
},
organizer: {
email: 'test@test.com',
self: true,
},
start: {
dateTime: '2023-02-02T22:30:00+03:00',
timeZone: 'Asia/Amman',
},
end: {
dateTime: '2023-02-02T23:30:00+03:00',
timeZone: 'Asia/Amman',
},
transparency: 'transparent',
iCalUID: '0nsfi5ttd2b17ac76ma2f37oi9@google.com',
sequence: 1,
attendees: [
{
email: 'attende@test.com',
responseStatus: 'needsAction',
},
{
email: 'test@test.com',
organizer: true,
self: true,
responseStatus: 'accepted',
},
],
reminders: {
useDefault: true,
},
eventType: 'default',
},
type: TriggerStrategy.POLLING,
async test({ store, auth, propsValue, files }) {
return await pollingHelper.test(polling, {
store,
auth,
propsValue: {
calendarId: propsValue.calendar_id,
expandRecurringEvent: propsValue.expandRecurringEvent,
},
files,
});
},
async onEnable({ store, auth, propsValue }) {
await pollingHelper.onEnable(polling, {
store,
auth,
propsValue: {
calendarId: propsValue.calendar_id,
expandRecurringEvent: propsValue.expandRecurringEvent,
},
});
},
async onDisable({ store, auth, propsValue }) {
await pollingHelper.onDisable(polling, {
store,
auth,
propsValue: {
calendarId: propsValue.calendar_id,
expandRecurringEvent: propsValue.expandRecurringEvent,
},
});
},
async run({ store, auth, propsValue, files }) {
return await pollingHelper.poll(polling, {
store,
auth,
propsValue: {
calendarId: propsValue.calendar_id,
expandRecurringEvent: propsValue.expandRecurringEvent,
},
files,
});
},
});

View File

@@ -0,0 +1,200 @@
import {
AppConnectionValueForAuthProperty,
createTrigger,
PiecePropValueSchema,
Property,
} from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { googleCalendarCommon } from '../common';
import { GoogleCalendarEvent } from '../common/types';
import { googleCalendarAuth } from '../../';
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
import { getEvents } from '../common/helper';
const polling: Polling<
AppConnectionValueForAuthProperty<typeof googleCalendarAuth>,
{
calendar_id: string | undefined;
specific_event: boolean | undefined;
event_id: string | undefined;
cancellation_reason: string[] | undefined;
}
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
const {
calendar_id: calendarId,
specific_event,
event_id,
cancellation_reason,
} = propsValue;
if (!calendarId) {
return [];
}
if (specific_event && !event_id) {
return [];
}
let minUpdated: Date;
if (lastFetchEpochMS === 0) {
minUpdated = new Date();
minUpdated.setDate(minUpdated.getDate() - 1);
} else {
minUpdated = new Date(lastFetchEpochMS);
}
let events: GoogleCalendarEvent[] = [];
if (specific_event && event_id) {
const eventRequest: HttpRequest = {
method: HttpMethod.GET,
url: `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events/${event_id}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.access_token,
},
};
try {
const eventResponse = await httpClient.sendRequest<GoogleCalendarEvent>(
eventRequest
);
const event = eventResponse.body;
const updatedTime = new Date(event.updated ?? 0).getTime();
if (updatedTime > lastFetchEpochMS && event.status === 'cancelled') {
events = [event];
}
} catch (error) {
console.error('Error fetching specific event:', error);
return [];
}
} else {
const allEvents = await getEvents(calendarId, true, auth, minUpdated);
events = allEvents.filter((event) => event.status === 'cancelled');
}
if (cancellation_reason && cancellation_reason.length > 0) {
events = events.filter((event) => {
return cancellation_reason.some((reason) => {
switch (reason) {
case 'deleted':
return !event.summary || event.summary.includes('Deleted');
case 'declined':
return (
event.attendees?.some(
(attendee) => attendee.responseStatus === 'declined'
) || false
);
case 'rescheduled':
return (
event.summary?.toLowerCase().includes('rescheduled') ||
event.description?.toLowerCase().includes('rescheduled') ||
false
);
case 'other':
return true;
default:
return true;
}
});
});
}
return events.map((event) => {
return {
epochMilliSeconds: new Date(event.updated!).getTime(),
data: event,
};
});
},
};
export const eventCancelled = createTrigger({
auth: googleCalendarAuth,
name: 'event_cancelled',
displayName: 'Event Cancelled',
description: 'Fires when an event is canceled or deleted.',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
specific_event: Property.Checkbox({
displayName: 'Target Specific Event',
description:
'Enable to monitor a specific event instead of all events in the calendar.',
required: false,
defaultValue: false,
}),
event_id: googleCalendarCommon.eventDropdown(false),
cancellation_reason: Property.StaticMultiSelectDropdown({
displayName: 'Cancellation Reasons',
description: 'Filter by specific types of cancellations (optional)',
required: false,
options: {
options: [
{ label: 'Event Deleted', value: 'deleted' },
{ label: 'Attendee Declined', value: 'declined' },
{ label: 'Event Rescheduled', value: 'rescheduled' },
{ label: 'Other Cancellations', value: 'other' },
],
},
}),
},
type: TriggerStrategy.POLLING,
sampleData: {
id: 'abc123def456_cancelled',
summary: 'Cancelled: Q3 Planning Session',
status: 'cancelled',
created: '2025-07-20T10:00:00.000Z',
updated: '2025-08-14T09:30:00.000Z',
organizer: { email: 'project.manager@example.com' },
start: { dateTime: '2025-08-25T10:00:00-07:00' },
end: { dateTime: '2025-08-25T11:30:00-07:00' },
},
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async run(context) {
return await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
async test(context) {
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
});

View File

@@ -0,0 +1,202 @@
import {
AppConnectionValueForAuthProperty,
createTrigger,
Property,
} from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { googleCalendarCommon } from '../common';
import { GoogleCalendarEvent } from '../common/types';
import { googleCalendarAuth } from '../../';
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
interface GoogleCalendarEventList {
items: GoogleCalendarEvent[];
}
const polling: Polling<
AppConnectionValueForAuthProperty<typeof googleCalendarAuth>,
{
calendar_id: string | undefined;
specific_event: boolean | undefined;
event_id: string | undefined;
}
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
if (lastFetchEpochMS === 0) {
return [];
}
const { calendar_id: calendarId, specific_event, event_id } = propsValue;
if (!calendarId) {
return [];
}
if (specific_event && !event_id) {
return [];
}
let events: GoogleCalendarEvent[] = [];
if (specific_event && event_id) {
const eventRequest: HttpRequest = {
method: HttpMethod.GET,
url: `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events/${event_id}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.access_token,
},
};
try {
const eventResponse = await httpClient.sendRequest<GoogleCalendarEvent>(
eventRequest
);
const event = eventResponse.body;
const endTimeString = event.end?.dateTime ?? event.end?.date;
if (endTimeString) {
const endTime = new Date(endTimeString).getTime();
if (endTime > lastFetchEpochMS) {
events = [event];
}
}
} catch (error) {
console.error('Error fetching specific event:', error);
return [];
}
} else {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${googleCalendarCommon.baseUrl}/calendars/${calendarId}/events`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.access_token,
},
queryParams: {
singleEvents: 'true',
orderBy: 'startTime',
timeMin: new Date(lastFetchEpochMS).toISOString(),
},
};
const response = await httpClient.sendRequest<GoogleCalendarEventList>(
request
);
events = response.body.items;
}
const endedEvents: {
epochMilliSeconds: number;
data: GoogleCalendarEvent;
}[] = [];
const now = Date.now();
for (const event of events) {
const endTimeString = event.end?.dateTime ?? event.end?.date;
if (!endTimeString) continue;
const endTime = new Date(endTimeString).getTime();
if (endTime > lastFetchEpochMS && endTime <= now) {
endedEvents.push({
epochMilliSeconds: endTime,
data: event,
});
}
}
return endedEvents;
},
};
export const eventEnds = createTrigger({
auth: googleCalendarAuth,
name: 'event_ends',
displayName: 'Event Ends',
description: 'Fires when an event ends.',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
specific_event: Property.Checkbox({
displayName: 'Target Specific Event',
description:
'Enable to monitor a specific event instead of all events in the calendar.',
required: false,
defaultValue: false,
}),
event_id: googleCalendarCommon.eventDropdown(false),
},
type: TriggerStrategy.POLLING,
sampleData: {
kind: 'calendar#event',
etag: '"3419997894982000"',
id: 'sample_event_id_67890',
status: 'confirmed',
htmlLink:
'https://www.google.com/calendar/event?eid=c2FtcGxlX2V2ZW50X2lkXzY3ODkw',
created: '2025-08-14T09:00:00.000Z',
updated: '2025-08-14T09:00:00.000Z',
summary: 'Project Deadline',
creator: { email: 'manager@example.com' },
organizer: {
email: 'manager@example.com',
self: true,
},
start: {
dateTime: '2025-08-14T14:30:00+05:30',
timeZone: 'Asia/Kolkata',
},
end: {
dateTime: '2025-08-14T15:30:00+05:30',
timeZone: 'Asia/Kolkata',
},
iCalUID: 'sample_event_id_67890@google.com',
sequence: 0,
reminders: { useDefault: true },
eventType: 'default',
},
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async run(context) {
return await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
async test(context) {
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
});

View File

@@ -0,0 +1,202 @@
import {
AppConnectionValueForAuthProperty,
createTrigger,
PiecePropValueSchema,
Property,
} from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { googleCalendarCommon } from '../common';
import { GoogleCalendarEvent } from '../common/types';
import { googleCalendarAuth } from '../../';
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
interface GoogleCalendarEventList {
items: GoogleCalendarEvent[];
}
const polling: Polling<
AppConnectionValueForAuthProperty<typeof googleCalendarAuth>,
{
calendar_id: string | undefined;
specific_event: boolean | undefined;
event_id: string | undefined;
time_value: number | undefined;
time_unit: string | undefined;
}
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue }) => {
const { calendar_id, specific_event, event_id, time_value, time_unit } =
propsValue;
if (!calendar_id || !time_value || !time_unit) {
return [];
}
if (specific_event && !event_id) {
return [];
}
let offset_ms = time_value * 60 * 1000;
if (time_unit === 'hours') {
offset_ms = time_value * 60 * 60 * 1000;
} else if (time_unit === 'days') {
offset_ms = time_value * 24 * 60 * 60 * 1000;
}
const now = Date.now();
const pollingIntervalMs = 5 * 60 * 1000;
const timeMin = new Date(now + offset_ms).toISOString();
const timeMax = new Date(now + offset_ms + pollingIntervalMs).toISOString();
let events: GoogleCalendarEvent[] = [];
if (specific_event && event_id) {
const eventRequest: HttpRequest = {
method: HttpMethod.GET,
url: `${googleCalendarCommon.baseUrl}/calendars/${calendar_id}/events/${event_id}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.access_token,
},
};
try {
const eventResponse = await httpClient.sendRequest<GoogleCalendarEvent>(
eventRequest
);
const event = eventResponse.body;
// Check if this specific event falls within our time window
const eventStartTime = new Date(
event.start?.dateTime ?? event.start?.date ?? 0
).getTime();
const triggerTime = eventStartTime - offset_ms;
if (triggerTime >= now && triggerTime <= now + pollingIntervalMs) {
events = [event];
}
} catch (error) {
console.error('Error fetching specific event:', error);
return [];
}
} else {
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${googleCalendarCommon.baseUrl}/calendars/${calendar_id}/events`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.access_token,
},
queryParams: {
singleEvents: 'true',
orderBy: 'startTime',
timeMin: timeMin,
timeMax: timeMax,
},
};
const response = await httpClient.sendRequest<GoogleCalendarEventList>(
request
);
events = response.body.items;
}
return events.map((event) => {
const startTime = new Date(
event.start?.dateTime ?? event.start?.date ?? 0
).getTime();
const triggerTime = startTime - offset_ms;
return {
epochMilliSeconds: triggerTime,
data: event,
};
});
},
};
export const eventStartTimeBefore = createTrigger({
auth: googleCalendarAuth,
name: 'event_starts_in',
displayName: 'Event Start (Time Before)',
description:
'Fires at a specified amount of time before an event starts (e.g., a reminder).',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
specific_event: Property.Checkbox({
displayName: 'Target Specific Event',
description:
'Enable to monitor a specific event instead of all events in the calendar.',
required: false,
defaultValue: false,
}),
event_id: googleCalendarCommon.eventDropdown(false),
time_value: Property.Number({
displayName: 'Time Before',
description: 'The amount of time before the event starts.',
required: true,
defaultValue: 15,
}),
time_unit: Property.StaticDropdown({
displayName: 'Time Unit',
required: true,
options: {
options: [
{ label: 'Minutes', value: 'minutes' },
{ label: 'Hours', value: 'hours' },
{ label: 'Days', value: 'days' },
],
},
defaultValue: 'minutes',
}),
},
type: TriggerStrategy.POLLING,
sampleData: {},
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async run(context) {
return await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
async test(context) {
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
});

View File

@@ -0,0 +1,203 @@
import {
createTrigger,
AppConnectionValueForAuthProperty,
Property,
} from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { googleCalendarAuth } from '../../';
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import { getCalendars } from '../common/helper';
import { CalendarObject } from '../common/types';
const polling: Polling<
AppConnectionValueForAuthProperty<typeof googleCalendarAuth>,
{
access_role_filter: string[] | undefined;
calendar_name_filter: string | undefined;
exclude_shared: boolean | undefined;
}
> = {
strategy: DedupeStrategy.LAST_ITEM,
items: async ({ auth, store, propsValue }) => {
const { access_role_filter, calendar_name_filter, exclude_shared } =
propsValue;
const currentCalendars = await getCalendars(auth);
let filteredCalendars = currentCalendars;
if (access_role_filter && access_role_filter.length > 0) {
filteredCalendars = filteredCalendars.filter((cal) =>
access_role_filter.includes(cal.accessRole)
);
}
if (calendar_name_filter && calendar_name_filter.trim()) {
const searchTerm = calendar_name_filter.toLowerCase().trim();
filteredCalendars = filteredCalendars.filter(
(cal) =>
cal.summary?.toLowerCase().includes(searchTerm) ||
cal.description?.toLowerCase().includes(searchTerm)
);
}
if (exclude_shared) {
filteredCalendars = filteredCalendars.filter(
(cal) => cal.accessRole === 'owner' || cal.primary
);
}
const currentCalendarIds = filteredCalendars.map((cal) => cal.id);
const oldCalendarIds = (await store.get<string[]>('calendars')) || [];
const oldCalendarIdsSet = new Set(oldCalendarIds);
const newCalendars = filteredCalendars.filter(
(cal) => !oldCalendarIdsSet.has(cal.id)
);
await store.put('calendars', currentCalendarIds);
return newCalendars.map((cal) => ({
id: cal.id,
data: {
...cal,
isOwned: cal.accessRole === 'owner' || cal.primary,
isShared: cal.accessRole !== 'owner' && !cal.primary,
calendarType: cal.primary
? 'primary'
: cal.accessRole === 'owner'
? 'owned'
: 'shared',
},
}));
},
};
export const newCalendar = createTrigger({
auth: googleCalendarAuth,
name: 'new_calendar',
displayName: 'New Calendar',
description: 'Fires when a new calendar is created or becomes accessible.',
props: {
access_role_filter: Property.StaticMultiSelectDropdown({
displayName: 'Access Role Filter',
description:
'Only trigger for calendars with specific access roles (optional)',
required: false,
options: {
options: [
{ label: 'Owner', value: 'owner' },
{ label: 'Writer', value: 'writer' },
{ label: 'Reader', value: 'reader' },
{ label: 'Free/Busy Reader', value: 'freeBusyReader' },
],
},
}),
calendar_name_filter: Property.ShortText({
displayName: 'Calendar Name Filter',
description:
'Only trigger for calendars containing this text in name or description (optional)',
required: false,
}),
exclude_shared: Property.Checkbox({
displayName: 'Exclude Shared Calendars',
description: 'Only trigger for calendars you own, not shared calendars',
required: false,
defaultValue: false,
}),
},
type: TriggerStrategy.POLLING,
sampleData: {
id: 'sample_calendar_id@group.calendar.google.com',
summary: 'New Project Team Calendar',
description: 'A shared calendar for the new project team.',
timeZone: 'Asia/Kolkata',
backgroundColor: '#9fe1e7',
foregroundColor: '#000000',
accessRole: 'owner',
isOwned: true,
isShared: false,
calendarType: 'owned',
primary: false,
},
async onEnable(context) {
const calendars = await getCalendars(context.auth);
const { access_role_filter, calendar_name_filter, exclude_shared } =
context.propsValue;
let filteredCalendars = calendars;
if (access_role_filter && access_role_filter.length > 0) {
filteredCalendars = filteredCalendars.filter((cal) =>
access_role_filter.includes(cal.accessRole)
);
}
if (calendar_name_filter && calendar_name_filter.trim()) {
const searchTerm = calendar_name_filter.toLowerCase().trim();
filteredCalendars = filteredCalendars.filter(
(cal) =>
cal.summary?.toLowerCase().includes(searchTerm) ||
cal.description?.toLowerCase().includes(searchTerm)
);
}
if (exclude_shared) {
filteredCalendars = filteredCalendars.filter(
(cal) => cal.accessRole === 'owner' || cal.primary
);
}
await context.store.put(
'calendars',
filteredCalendars.map((cal) => cal.id)
);
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async run(context) {
return await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
async test(context) {
const calendars = await getCalendars(context.auth);
const recentCalendars = calendars.slice(-1);
return recentCalendars.map((cal: CalendarObject) => ({
id: cal.id,
data: {
...cal,
isOwned: cal.accessRole === 'owner' || cal.primary,
isShared: cal.accessRole !== 'owner' && !cal.primary,
calendarType: cal.primary
? 'primary'
: cal.accessRole === 'owner'
? 'owned'
: 'shared',
},
}));
},
});

View File

@@ -0,0 +1,226 @@
import {
createTrigger,
AppConnectionValueForAuthProperty,
Property,
} from '@activepieces/pieces-framework';
import { TriggerStrategy } from '@activepieces/pieces-framework';
import { googleCalendarCommon } from '../common';
import { GoogleCalendarEvent } from '../common/types';
import { googleCalendarAuth } from '../../';
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
} from '@activepieces/pieces-common';
interface GoogleCalendarEventList {
items: GoogleCalendarEvent[];
}
const polling: Polling<
AppConnectionValueForAuthProperty<typeof googleCalendarAuth>,
{
calendar_id: string | undefined;
search_term: string | undefined;
event_types: string[] | undefined;
search_fields: string[] | undefined;
}
> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
const { calendar_id, search_term, event_types, search_fields } = propsValue;
if (!calendar_id || !search_term) {
return [];
}
let minUpdated: Date;
if (lastFetchEpochMS === 0) {
minUpdated = new Date();
minUpdated.setDate(minUpdated.getDate() - 1);
} else {
minUpdated = new Date(lastFetchEpochMS);
}
const queryParams: Record<string, string> = {
singleEvents: 'true',
orderBy: 'updated',
updatedMin: minUpdated.toISOString(),
q: search_term,
};
if (event_types && event_types.length > 0) {
event_types.forEach((type) => {
if (!queryParams.eventTypes) {
queryParams.eventTypes = type;
} else {
queryParams.eventTypes += '&eventTypes=' + type;
}
});
}
const request: HttpRequest = {
method: HttpMethod.GET,
url: `${googleCalendarCommon.baseUrl}/calendars/${calendar_id}/events`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: auth.access_token,
},
queryParams: queryParams,
};
const response = await httpClient.sendRequest<GoogleCalendarEventList>(
request
);
const events = response.body.items;
const newEvents = events.filter((event) => {
const created = new Date(event.created ?? 0).getTime();
const updated = new Date(event.updated ?? 0).getTime();
const isNewEvent = updated - created < 5000;
if (!isNewEvent) return false;
if (search_fields && search_fields.length > 0) {
const searchTermLower = search_term.toLowerCase();
return search_fields.some((field) => {
switch (field) {
case 'summary':
return (
event.summary?.toLowerCase().includes(searchTermLower) || false
);
case 'description':
return (
event.description?.toLowerCase().includes(searchTermLower) ||
false
);
case 'location':
return (
event.location?.toLowerCase().includes(searchTermLower) || false
);
case 'attendees':
return (
event.attendees?.some(
(attendee) =>
attendee.email?.toLowerCase().includes(searchTermLower) ||
attendee.displayName
?.toLowerCase()
.includes(searchTermLower)
) || false
);
default:
return false;
}
});
}
return true;
});
return newEvents.map((event) => {
return {
epochMilliSeconds: new Date(event.updated!).getTime(),
data: event,
};
});
},
};
export const newEventMatchingSearch = createTrigger({
auth: googleCalendarAuth,
name: 'new_event_matching_search',
displayName: 'New Event Matching Search',
description:
'Fires when a new event is created that matches a specified search term.',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
search_term: Property.ShortText({
displayName: 'Search Term',
description:
'The keyword(s) to search for in new events (searches across title, description, location, and attendees by default).',
required: true,
}),
event_types: Property.StaticMultiSelectDropdown({
displayName: 'Event Types',
description: 'Filter by specific event types (optional)',
required: false,
options: {
options: [
{ label: 'Default Events', value: 'default' },
{ label: 'Birthday Events', value: 'birthday' },
{ label: 'Focus Time', value: 'focusTime' },
{ label: 'Out of Office', value: 'outOfOffice' },
{ label: 'Working Location', value: 'workingLocation' },
{ label: 'From Gmail', value: 'fromGmail' },
],
},
}),
search_fields: Property.StaticMultiSelectDropdown({
displayName: 'Search In Fields',
description:
"Specify which fields to search in (leave empty to use Google's default search across all fields)",
required: false,
options: {
options: [
{ label: 'Event Title/Summary', value: 'summary' },
{ label: 'Event Description', value: 'description' },
{ label: 'Event Location', value: 'location' },
{ label: 'Attendee Names/Emails', value: 'attendees' },
],
},
}),
},
type: TriggerStrategy.POLLING,
sampleData: {
id: 'abc123def456',
summary: 'Final Project Review',
description: 'Review of the Q3 final project deliverables.',
status: 'confirmed',
created: '2025-08-14T09:05:00.000Z',
updated: '2025-08-14T09:05:01.000Z',
start: { dateTime: '2025-09-01T10:00:00-07:00' },
end: { dateTime: '2025-09-01T11:30:00-07:00' },
organizer: { email: 'project.manager@example.com' },
},
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async run(context) {
return await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
async test(context) {
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
});

View File

@@ -0,0 +1,186 @@
import {
createTrigger,
TriggerStrategy,
OAuth2PropertyValue,
Property,
} from '@activepieces/pieces-framework';
import { googleCalendarAuth } from '../../';
import { googleCalendarCommon } from '../common';
import { stopWatchEvent, watchEvent, getLatestEvent } from '../common/helper';
import { GoogleWatchResponse, GoogleCalendarEvent } from '../common/types';
export const newEvent = createTrigger({
auth: googleCalendarAuth,
name: 'new_event',
displayName: 'New Event',
description: 'Fires when a new event is created in a calendar.',
props: {
calendar_id: googleCalendarCommon.calendarDropdown('writer'),
event_types: Property.StaticMultiSelectDropdown({
displayName: 'Event Types to Monitor',
description:
'Filter by specific event types (leave empty to monitor all event types)',
required: false,
options: {
options: [
{ label: 'Default Events', value: 'default' },
{ label: 'Birthday Events', value: 'birthday' },
{ label: 'Focus Time', value: 'focusTime' },
{ label: 'Out of Office', value: 'outOfOffice' },
{ label: 'Working Location', value: 'workingLocation' },
{ label: 'From Gmail', value: 'fromGmail' },
],
},
}),
search_filter: Property.ShortText({
displayName: 'Search Filter',
description:
'Only trigger for events containing this text in title, description, or location (optional)',
required: false,
}),
exclude_all_day: Property.Checkbox({
displayName: 'Exclude All-Day Events',
description: 'Skip triggering for all-day events',
required: false,
defaultValue: false,
}),
},
type: TriggerStrategy.WEBHOOK,
sampleData: {
kind: 'calendar#event',
etag: '"3419997894982000"',
id: 'sample_event_id_12345',
status: 'confirmed',
htmlLink:
'https://www.google.com/calendar/event?eid=c2FtcGxlX2V2ZW50X2lkXzEyMzQ1',
created: '2025-08-15T11:02:27.000Z',
updated: '2025-08-15T11:02:27.491Z',
summary: 'Team Sync',
creator: { email: 'creator@example.com' },
organizer: {
email: 'creator@example.com',
self: true,
},
start: {
dateTime: '2025-08-18T10:00:00-07:00',
timeZone: 'America/Los_Angeles',
},
end: {
dateTime: '2025-08-18T11:00:00-07:00',
timeZone: 'America/Los_Angeles',
},
iCalUID: 'sample_event_id_12345@google.com',
sequence: 0,
reminders: { useDefault: true },
eventType: 'default',
},
async onEnable(context) {
const calendarId = context.propsValue.calendar_id!;
const auth = context.auth as OAuth2PropertyValue;
const response = await watchEvent(calendarId, context.webhookUrl, auth);
await context.store.put<GoogleWatchResponse>(
'google_calendar_watch',
response
);
},
async onDisable(context) {
const auth = context.auth as OAuth2PropertyValue;
const watch = await context.store.get<GoogleWatchResponse>(
'google_calendar_watch'
);
if (watch) {
await stopWatchEvent(watch, auth);
}
},
async run(context) {
const payload = context.payload;
const headers = payload.headers as Record<string, string>;
const { event_types, search_filter, exclude_all_day } = context.propsValue;
if (headers['x-goog-resource-state'] === 'add') {
const eventData = payload.body as GoogleCalendarEvent;
if (event_types && event_types.length > 0) {
const eventType = eventData.eventType || 'default';
if (!event_types.includes(eventType)) {
return [];
}
}
if (search_filter && search_filter.trim()) {
const searchTerm = search_filter.toLowerCase().trim();
const summary = (eventData.summary || '').toLowerCase();
const description = (eventData.description || '').toLowerCase();
const location = (eventData.location || '').toLowerCase();
const matchesSearch =
summary.includes(searchTerm) ||
description.includes(searchTerm) ||
location.includes(searchTerm);
if (!matchesSearch) {
return [];
}
}
if (exclude_all_day) {
const isAllDay = eventData.start?.date && !eventData.start?.dateTime;
if (isAllDay) {
return [];
}
}
return [eventData];
} else {
return [];
}
},
async test(context) {
const auth = context.auth as OAuth2PropertyValue;
const { event_types, search_filter, exclude_all_day } = context.propsValue;
const latestEvent = await getLatestEvent(
context.propsValue.calendar_id!,
auth
);
if (event_types && event_types.length > 0) {
const eventType = latestEvent.eventType || 'default';
if (!event_types.includes(eventType)) {
return [];
}
}
if (search_filter && search_filter.trim()) {
const searchTerm = search_filter.toLowerCase().trim();
const summary = (latestEvent.summary || '').toLowerCase();
const description = (latestEvent.description || '').toLowerCase();
const location = (latestEvent.location || '').toLowerCase();
const matchesSearch =
summary.includes(searchTerm) ||
description.includes(searchTerm) ||
location.includes(searchTerm);
if (!matchesSearch) {
return [];
}
}
if (exclude_all_day) {
const isAllDay = latestEvent.start?.date && !latestEvent.start?.dateTime;
if (isAllDay) {
return [];
}
}
return [latestEvent];
},
});