В данной статье рассмотрим, как можно создать, обновить, удалить или найти актуальные данные события в Google Calendar с помощью функциональных кнопок в сделке и использования механизма скриптов App Script
Задача
Настроить функциональные кнопки. Каждая кнопка будет осуществлять определенное действие:
- Создавать событие в Google Calendar
- Обновлять событие в Google Calendar
- Искать событие в Google Calendar
- Удалять событие в Google Calendar
Подключение AmoCRM
Создание полей
Перед подключением AmoCRM в MoreKIT — необходимо создать поля, с которыми будет осуществляться взаимодействие между MoreKIT и событиями Google Calendar

Будут использоваться следующие поля:
- ID события — текст
- Дата начала встречи — дата и время
- Дата конца встречи — дата и время
- Название — текст
- Описание — текст
- Локация — текст
- Гость может приглашать других — флаг
- Гость может редактировать событие — флаг
- Гость может видеть других гостей — флаг
Подключение сервиса
В MoreKit в разделе Сервисы — ищем в списке сервисов amoCRM, нажимаем на кнопку Добавить, произойдет перенаправление на страницу переадресации amoCRM, где необходимо выбрать нужную CRM, которую надо подключить в MoreKit


Подключение виджета TapKit в amoCRM
В amoCRm переходим в amoМаркет, вводим в поиске виджетов Tap Kit, устанавливаем виджет

Настройка App Script
Следующим шагом — создадим проект в App Script и настроим скрипты для управления событиями:
- Зайдите на страницу https://script.google.com/home
- Нажмите на кнопку Создать Проект
В App Script у вас будет окно с текущими скриптами, а также возможность подключать дополнительные сервисы
Нам необходимо установить Google Calendar API

Google App Script предоставляет довольно мощный функционал для работы со скриптами, на случай если понадобится изменить логику отправки и обработки отправляемых данных скриптом. В случае такой необходимости — советуем обратиться к официальной документации Google appScript, где описаны возможные методы и принцип работы App Script
Структура проекта
В данном кейсе — будет использоваться 7 скриптов
- Config.gs
- CreateEvent.gs
- FindEvent.gs
- UpdateEvent.gs
- DeleteEvent.gs
- Code.gs
И вспомогательный одноразовый скрипт Выгрузка календарей.gs
Устанавливаем скрипты
function listCalendars() {
const calendars = CalendarApp.getAllCalendars();
Logger.log("Найдено календарей: " + calendars.length);
calendars.forEach(function(calendar, index) {
Logger.log("Календарь #" + (index + 1));
Logger.log("Название : " + calendar.getName());
Logger.log("ID : " + calendar.getId());
Logger.log("Описание : " + calendar.getDescription());
Logger.log("Часовой пояс: " + calendar.getTimeZone());
Logger.log("Цвет : " + calendar.getColor());
Logger.log("Скрыт : " + calendar.isHidden());
});
Logger.log("Календарь по умолчанию:");
Logger.log(CalendarApp.getDefaultCalendar().getName());
Logger.log("ID: " + CalendarApp.getDefaultCalendar().getId());
}const DEFAULT_CALENDAR = "primary";
function json(obj) {
return ContentService
.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}function createEvent(data) {
// ID календаря
let calendarId = DEFAULT_CALENDAR;
if (data.calendarId) {
calendarId = data.calendarId;
} else if (data.calendarName) {
const calendars = CalendarApp.getCalendarsByName(data.calendarName);
if (calendars.length === 0)
throw new Error("Календарь не найден");
calendarId = calendars[0].getId();
}
// Валидация
if (!data.summary)
throw new Error("summary не заполнено");
if (!data.startDate)
throw new Error("startDate не заполнено");
if (!data.endDate)
throw new Error("endDate не заполнено");
// Формируем ресурс события
const resource = {
summary: data.summary,
description: data.description || "",
location: data.location || ""
};
// Дата/время
const timeZone = data.timeZone || Session.getScriptTimeZone();
if (data.allDay) {
resource.start = { date: new Date(data.startDate).toISOString().split("T")[0] };
resource.end = { date: new Date(data.endDate).toISOString().split("T")[0] };
} else {
resource.start = {
dateTime: new Date(data.startDate).toISOString(),
timeZone: timeZone
};
resource.end = {
dateTime: new Date(data.endDate).toISOString(),
timeZone: timeZone
};
}
// Гости
if (Array.isArray(data.guests)) {
resource.attendees = data.guests.map(function(email) {
if (typeof email === "string")
return { email: email };
return email; // объект вида { email: "x", optional: true }
});
}
// Видимость
if (data.visibility) {
resource.visibility = data.visibility; // "default" | "public" | "private" | "confidential"
}
// Настройки гостей
if (typeof data.guestsCanInviteOthers === "boolean")
resource.guestsCanInviteOthers = data.guestsCanInviteOthers;
if (typeof data.guestsCanModify === "boolean")
resource.guestsCanModify = data.guestsCanModify;
if (typeof data.guestsCanSeeGuests === "boolean")
resource.guestsCanSeeGuests = data.guestsCanSeeGuests;
// Цвет (Calendar API использует colorId — строка от "1" до "11")
if (data.color) {
resource.colorId = String(data.color);
}
// Напоминания
if (Array.isArray(data.reminders)) {
resource.reminders = {
useDefault: false,
overrides: data.reminders.map(function(r) {
return {
method: r.method, // "popup" или "email"
minutes: r.minutes
};
})
};
} else {
resource.reminders = { useDefault: true, overrides: [] };
}
const event = Calendar.Events.insert(resource, calendarId);
// Ответ
return json({
success: true,
eventId: event.id,
iCalUID: event.iCalUID,
calendarId: calendarId
});
}function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
switch ((data.action || "").toLowerCase()) {
case "create":
return createEvent(data);
case "find":
return findEvent(data);
case "update":
return updateEvent(data);
case "delete":
return deleteEvent(data);
default:
throw new Error("Неизвестное действие: " + data.action);
}
} catch (err) {
return json({ success: false, error: String(err) });
}
}function findEvent(data) {
// Календарь
let calendarId = DEFAULT_CALENDAR;
if (data.calendarId) {
calendarId = data.calendarId;
} else if (data.calendarName) {
const calendars = CalendarApp.getCalendarsByName(data.calendarName);
if (calendars.length === 0)
throw new Error("Календарь не найден");
calendarId = calendars[0].getId();
}
// Валидация
if (!data.eventId)
throw new Error("eventId не заполнено");
// Получаем событие через Calendar API
let event;
try {
event = Calendar.Events.get(calendarId, data.eventId);
} catch (err) {
const msg = String(err);
if (msg.indexOf("404") >= 0 || msg.indexOf("Not Found") >= 0) {
return json({ success: true, found: false });
}
throw err;
}
// Если событие удалено — not found
if (event.status === "cancelled") {
return json({ success: true, found: false });
}
// Формируем ответ
return json({
success: true,
found: true,
event: {
eventId: event.id,
summary: event.summary || "",
description: event.description || "",
location: event.location || "",
startDate: event.start.dateTime || event.start.date,
endDate: event.end.dateTime || event.end.date,
allDay: !!event.start.date,
lastUpdated: event.updated,
status: event.status,
htmlLink: event.htmlLink,
guests: (event.attendees || []).map(function(g) {
return {
email: g.email,
responseStatus: g.responseStatus,
optional: !!g.optional
};
})
}
});
}function updateEvent(data) {
// Календарь
let calendarId = DEFAULT_CALENDAR;
if (data.calendarId) {
calendarId = data.calendarId;
} else if (data.calendarName) {
const calendars = CalendarApp.getCalendarsByName(data.calendarName);
if (calendars.length === 0)
throw new Error("Календарь не найден");
calendarId = calendars[0].getId();
}
// Валидация
if (!data.eventId)
throw new Error("eventId не заполнено");
// Получаем событие через Calendar API
let event;
try {
event = Calendar.Events.get(calendarId, data.eventId);
} catch (err) {
const msg = String(err);
if (msg.indexOf("404") >= 0 || msg.indexOf("Not Found") >= 0) {
return json({ success: false, error: "Событие не найдено" });
}
throw err;
}
// Обновляем поля
const resource = {};
if (data.summary) resource.summary = data.summary;
if (data.description) resource.description = data.description;
if (data.location) resource.location = data.location;
if (data.startDate || data.endDate) {
resource.start = {};
resource.end = {};
if (data.startDate) {
if (data.allDay) {
resource.start.date = new Date(data.startDate).toISOString().split("T")[0];
resource.end.date = new Date(data.endDate || data.startDate).toISOString().split("T")[0];
} else {
resource.start.dateTime = new Date(data.startDate).toISOString();
resource.end.dateTime = new Date(data.endDate || data.startDate).toISOString();
}
// Часовой пояс - либо из запроса, либо из календаря
if (data.timeZone) {
resource.start.timeZone = data.timeZone;
resource.end.timeZone = data.timeZone;
}
}
}
if (Array.isArray(data.guests)) {
resource.attendees = data.guests.map(function(email) {
return { email: email };
});
}
if (data.visibility) {
resource.visibility = data.visibility;
}
Calendar.Events.patch(resource, calendarId, data.eventId);
// Возвращаем обновлённое событие
const updated = Calendar.Events.get(calendarId, data.eventId);
return json({
success: true,
event: {
eventId: updated.id,
summary: updated.summary || "",
description: updated.description || "",
location: updated.location || "",
startDate: updated.start.dateTime || updated.start.date,
endDate: updated.end.dateTime || updated.end.date,
allDay: !!updated.start.date,
lastUpdated: updated.updated,
status: updated.status,
htmlLink: updated.htmlLink
}
});
}function deleteEvent(data) {
// Календарь
let calendarId = DEFAULT_CALENDAR;
if (data.calendarId) {
calendarId = data.calendarId;
} else if (data.calendarName) {
const calendars = CalendarApp.getCalendarsByName(data.calendarName);
if (calendars.length === 0)
throw new Error("Календарь не найден");
calendarId = calendars[0].getId();
}
// Валидация
if (!data.eventId)
throw new Error("eventId не заполнено");
// Удаление
try {
Calendar.Events.remove(calendarId, data.eventId);
} catch (err) {
const msg = String(err);
if (msg.indexOf("404") >= 0 || msg.indexOf("Not Found") >= 0) {
return json({ success: false, error: "Событие не найдено" });
}
throw err;
}
return json({ success: true });
}Развертывание скрипта
Следующим шагом — выполним развертывание скрипта, чтобы он начал работать с календарем

Если скрипт попросит выдаст права на выполнение — подтверждаем

Оставьте данное окно, позже потребуется ссылка на Веб-приложение
Создание процессов MoreKIT
В MoreKIT нам потребуется создать 4 процесса:
- Процесс создания события
- Процесс поиска события
- Процесс удаления события
- Процесс обновления события
Для создания процесса нажмите на кнопку Создать процесс, тип процесса выбираем Другое, даем процессу название и сохраняем

Внутри каждого процесса есть принимающая webhook ссылка, она потребуется нам для настройки виджета Tap KIT


Следующим шагом — перейдем к настройке виджета Tap KIT
Настройка Tap KIT
В настройках виджета TapKIT — создадим 4 кнопки

Пример настройки кнопки:
- Тип карточки: Сделки — сущность, в которой будет отображаться кнопка
- Расположение: ID события — где будут располагаться кнопки, в данном примере они будут располагаться около кнопки ID события
- Содержимое: Сделки — набор данных, которые будут отправляться в MoreKIT. В данном случае — сделки
- Текст кнопки — текст кнопки, отображающий процесс кнопки, который она выполняет
- Адрес отправки: здесь необходимо вставить ссылку из первого блока соответствующего процесса из MoreKIT
- Цвет кнопки: цвет кнопки
- Цвет текста: цвет текста
- Воронки и статусы: где будет отображаться кнопка
- Группы пользователей: для кого кнопка должна показываться, а для кого нет

В результате получим функциональные кнопки, которые будут управлять процессами
Процессы в MoreKIT
После настройки кнопок — настроим процессы в MoreKIT
Процесс создания события
Схема процесса по созданию события

Описание блоков схемы

#2 Найти сделки — находим полные данные о сделке
#3 Найти контакты — Ищем контакт из сделки, он будет нашим гостем в событии
#8 Конвертировать дату — преобразуем дату из формата timestamp, который отдает нам amoCRM для формата App Script
Формат для App Script, который используется: %Y-%m-%d %H:%M:%S +0700. Часовой пояс здесь +0700 нивелирует разницу между часовым поясом amoCRM и аккаунтом Google. Если время в событиях не будет совпадать с датами, указанными в сделке amoCRM — измените эту цифру
#9 Добавить в список — данные блок собирает список гостей, в значение кладем Email контакта amoCRM

Данный блок будет отправлять запрос на App Script
URL: Скопируйте ссылку из развертывания (во всех процессах она будет одинаковая)
Метод: POST
Доступные параметры:
{
//Обязательные поля
"action": "create", // Действие для скрипта (обязательно)
"summary": "string", // Заголовок события (обязательно)
"startDate": "string", // Начало: "2026-07-07T10:00:00+05:00" или "2026-07-07" при allDay (обязательно)
"endDate": "string", // Конец: "2026-07-07T11:00:00+05:00" или "2026-07-07" при allDay (обязательно)
// Календарь (опционально, по умолч. primary)
"calendarId": "string", // ID календаря, e.g. "mycalendar@gmail.com"
// "calendarName": "string", // Или название календаря (если нет calendarId)
//Описание события (опционально)
"description": "string", // Описание, e.g. "Обсуждение договора"
"location": "string", // Место, e.g. "Офис, 3 этаж"
//Дата/время (опционально)
"allDay": false, // true — событие на весь день
"timeZone": "string", // Часовой пояс, e.g. "Asia/Yekaterinburg" (по умолч. ScriptTimeZone)
//Гости (опционально)
"guests": [ // Массив строк (email)
"ivanov@example.com",
"petrov@example.com"
],
// Настройки приватности (опционально)
"visibility": "default", // "default" | "public" | "private" | "confidential"
"color": 1, // Цвет события: число от 1 до 11
// Права гостей (опционально) ---
"guestsCanInviteOthers": false, // Гости могут приглашать других
"guestsCanModify": false, // Гости могут редактировать событие
"guestsCanSeeGuests": true, // Гости видят список остальных гостей
//Напоминания (опционально)
"reminders": [ // Массив напоминаний
{ "method": "popup", "minutes": 30 }, // Всплывающее уведомление
{ "method": "email", "minutes": 60 } // Уведомление на email
]
}Если вы хотите создать событие в другом календаре — запустите вручную скрипт Выгрузка календарей.gs

Скрипт выгрузит все календари, разрешенные на вашем аккаунте
И оставшаяся часть блоков:

#11 JSON декодировать — данный блок достает ответ от HTTP блока запроса
В нем будет содержаться следующая структура:
{
"item":{
"success":true, //результат, создано событие или нет
"eventId":"3j290fodjk1qrtjeb4mfrdo1p4@google.com", //ID события
"iCalUID":"3j290fodjk1qrtjeb4mfrdo1p4@google.com",//iCalUID
"calendarId":"mycalendar@gmail.com"//ID календаря
}
}#12 Регулярные выражения — скрипт отдает ID события в формате <ID>@google.com, но @google.com не считается частью ID события, данный блок обрежет эту часть строки, оставляя только ID события, т.е. на выходе получится: 3j290fodjk1qrtjeb4mfrdo1p4
#10 Обновить сделку — данный блок записывает в поле ID события в amoCRM ID созданного события
Процесс обновления события
Процесс обновления события актуальными данными из сделки будет выглядеть следующим образом:

Описание блоков схемы
Первая часть блоков аналогична блокам по созданию события

Параметры, доступные для обновления события аналогичны параметрам создания события, но дополнительно добавляется eventId — в него передается ID события из сделки amoCRM
Также не забудьте поправить action на update, чтобы скрипт понимал, что это запрос на обновление события

#8 JSON декодировать — раскладывает ответ от HTTP запрос в JSON формат
В ответе будут актуальные данные обновленного события и ключ success с результатом обновления
{
"item":{
"success":true,
"event":{
"eventId":"hteu82jslj4iron567umbml8tk",
"summary":"Событие из амо",
"description":"Здесь находится описание события",
"location":"",
"startDate":"2026-07-06T16:15:00+05:00",
"endDate":"2026-07-06T18:15:00+05:00",
"allDay":false,
"lastUpdated":"2026-07-06T10:51:02.641Z",
"status":"confirmed",
"htmlLink":"https://www.google.com/calendar/event?eid=aHRldTgyanNsajRpcm9uNTY3dW1ibWw4dGsgYW5kcmV5bW9yZWtpdEBt"
}
}
}#11 Проверка — проверяет успешность выполнения. В зависимости от проверки — будет прикрепляться свое примечание в сделку amoCRM
#10 Создать примечание — отправляем в примечание сделки amoCRM текст об успешном создании события, прикрепляя ссылку на событие
#12 Создать примечание — отправляем в примечание сделки amoCRM текст об ошибке, прикрепляя данные ошибки
Процесс удаления события
Процесс удаления события будет выглядеть следующим образом:

Данный процесс будет удалять событие, очищать поля сделки от данных по событию после удаления и прикреплять примечание об удаленном событии
Описание блоков схемы

Блоки #2 и #3 также аналогичны процессам по созданию или обновления события
#4 HTTP запрос — данный блок отправляет "action": "delete" и ID события, чтобы скрипт удалил событие

#5 JSON декодировать — получаем ответ от HTTP запроса
#6 Проверка — проверяет, удалилось событие или нет
#7 Создать примечание — если событие удалилось, прикрепляет в сделку примечание об успешном удалении
#9 Обновить сделку — очищает поля в сделке amoCRM, связанные с событием
#8 Создать примечание — если событие не удалилось, прикрепляет примечание в сделку с текстом ошибки
Процесс поиска события
Процесс поиска события будет выглядеть следующим образом:

Блоки #209 и #210 аналогичны процессам обновления и создания события

#211 HTTP запрос — отправляет на скрипт ID события из сделки, а также событие "action": "find"
#212 JSON-декодировать — получает ответ от HTTP запроса с данными
#214 Проверка — проверяет, нашлось событие или нет
#215 Создать примечание — создает примечание к сделке с данными о событии
#216 Создать примечание — создает примечание к сделке с сообщением о том, что событие не найдено
Тестирование
Создание событие
Перейдем в тестовую сделку amoCRM и заполним данные, по которым будет создаваться событие

Нажимаем на кнопку Tap KIT Создать событие и проверяем календарь

Как видим — событие успешно создалось
В MoreKIT также можно увидеть событие, если перейти в События из процесса по созданию событий


В сделку должен был записаться ID созданного события

Если ID есть — значит все настроено верно
Обновление события
Попробуем сменить даты события — проверим, сместится ли оно. Переставим даты на 2 часа назад и нажмем на кнопку Обновить событие

Проверим, сместилось ли оно в календаре

Событие обновилось
Найти событие
Следующим шагом — получим актуальные данные события. Нажмем на кнопку Найти событие

В текущем примере — данные создаются в примечании сделки, при желании — вы можете либо заполнять поля актуальными данными, либо настроить автоматизированный запуск процессом с типом По времени
Удаление события
Следующим шагом — удалим событие через кнопку и проверим, чтобы оно удалилось на стороне календаря

После нажатия — поля в сделке должны очиститься

На стороне календаря также видим, что событие удалилось
Подведем итоги
В данной статье рассмотрели пример интеграции с Google Calendar с помощью виджета TapKIT. Логика интеграции завязана на кнопках
Было сделано:
- создание события
- обновление события
- получение данных о событии
- удаление события
Данный скрипт дает большой простор для интеграции, вы можете подстроить его под свой бизнес процесс, например — настроить автоматическую синхронизацию с помощью процесса По времени, либо автоматически создавать события по встроенным триггерам в amoCRM