Интеграция Google Calendar и n8n: встречи без дублей и ошибок timezone ¶
Обновлено: 2026-05-30
Импортируйте JSON в n8n, замените credentials, IDs, правила доступа и production-политики под вашу инфраструктуру.
Проблема: автоматическая запись на встречу через Google Calendar кажется простой, пока workflow не создаёт два event, не путает часовой пояс клиента и не отправляет лишнее приглашение всем участникам.
Решение: интеграция Google Calendar и n8n должна сначала нормализовать входной слот, проверить freeBusy, посчитать idempotency key, найти уже созданный event и только потом создавать или обновлять встречу. Такой подход закрывает не демонстрационный happy path, а реальную production-боль: повторы, права доступа, пустые поля, API-ошибки и ручной контроль там, где автоматизация может навредить.
Проблема: почему простая связка ломает процесс ¶
Интеграция нужна не ради факта подключения сервиса к n8n. Пользователь ищет конкретный ответ: как настроить сценарий так, чтобы данные не дублировались, права не были избыточными, а результат можно было проверить без ручного расследования execution logs.
Для этой страницы основной объект — calendar event. Входной контракт должен явно фиксировать lead_id, calendar_id, start, end, timezone, attendees. Если эти поля приходят нестабильно, автоматизация начинает угадывать, а угадывание в production почти всегда превращается в дубли, потерю данных или лишние уведомления.
Поэтому workflow строится вокруг детерминированных проверок: сначала validation и idempotency, потом запрос к API, потом запись результата и только после этого уведомление человека или downstream-системы.
Архитектура workflow для n8n ¶
| Блок | Задача | Production-проверка |
|---|---|---|
| Webhook / CRM trigger | принимает слот от формы, CRM или booking-виджета | есть lead_id и timezone |
| Normalize event | проверяет start/end, attendees и policy | ISO datetime с offset, end > start |
| Idempotency lookup | ищет созданный event по ключу | event_id хранится в CRM/Postgres |
| Google Calendar freeBusy | проверяет занятость календаря | не бронируем занятый слот |
| Create or update event | создаёт или обновляет встречу | контролируем sendUpdates и Meet |
| Respond + audit | возвращает status и event_id | есть execution_id и причина отказа |
Такой workflow удобно сопровождать: каждая нода отвечает за один слой ответственности, а не смешивает mapping, API-запрос, retry и уведомления в одном Code Node.
Контракт входных данных ¶
{
"lead_id": "lead-4812",
"calendar_id": "sales@example.com",
"title": "Demo call: Acme / n8n",
"start": "2026-06-02T10:00:00+03:00",
"end": "2026-06-02T10:45:00+03:00",
"timezone": "Europe/Moscow",
"attendees": [
"client@example.com",
"manager@example.com"
],
"source": "tilda",
"send_updates": "all"
}Payload можно расширять, но нельзя делать обязательные поля “по настроению”. Если источник не передал внешний ID, timezone, владельца или другой ключевой атрибут, workflow должен остановиться с понятной ошибкой до записи во внешний сервис.
Code Node: нормализация, mapping и guard-условия ¶
const src = $json.body ?? $json;
const required = ['lead_id', 'calendar_id', 'start', 'end', 'timezone'];
for (const key of required) {
if (!src[key]) throw new Error(`Missing ${key} for Calendar booking`);
}
const start = new Date(src.start);
const end = new Date(src.end);
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
throw new Error('Invalid ISO datetime for Google Calendar event');
}
if (end <= start) throw new Error('Event end must be after start');
const attendees = Array.isArray(src.attendees) ? src.attendees.filter(Boolean) : [];
const idempotencyKey = `calendar:${src.calendar_id}:${src.lead_id}:${start.toISOString()}`;
return [{
json: {
action: 'check_freebusy_then_upsert_event',
idempotency_key: idempotencyKey,
calendar_id: src.calendar_id,
event: {
summary: String(src.title ?? 'Встреча').trim(),
start: { dateTime: src.start, timeZone: src.timezone },
end: { dateTime: src.end, timeZone: src.timezone },
attendees: attendees.map(email => ({ email })),
description: `Source=${src.source ?? 'n8n'}; lead_id=${src.lead_id}; key=${idempotencyKey}`
},
send_updates: src.send_updates ?? 'all'
}
}];Этот скрипт n8n не заменяет бизнес-логику внешнего сервиса. Его задача — привести данные к стабильному контракту, сформировать idempotency key и не пропустить опасный payload дальше по цепочке.
Готовый workflow JSON: скачать и импортировать ¶
В архиве страницы есть импортируемый workflow JSON и тестовый payload. После импорта замените credentials, URL, IDs, папки, владельцев, лимиты и правила доступа. Не запускайте сценарий на production-данных, пока не проверены повторы, пустые значения и ошибки API.
{
"name": "Nodbot - Google Calendar booking with freeBusy and idempotency",
"nodes": [
{
"name": "Webhook / CRM trigger",
"type": "n8n-node",
"purpose": "принимает слот от формы, CRM или booking-виджета"
},
{
"name": "Normalize event",
"type": "n8n-node",
"purpose": "проверяет start/end, attendees и policy"
},
{
"name": "Idempotency lookup",
"type": "n8n-node",
"purpose": "ищет созданный event по ключу"
},
{
"name": "Google Calendar freeBusy",
"type": "n8n-node",
"purpose": "проверяет занятость календаря"
},
{
"name": "Create or update event",
"type": "n8n-node",
"purpose": "создаёт или обновляет встречу"
},
{
"name": "Respond + audit",
"type": "n8n-node",
"purpose": "возвращает status и event_id"
}
],
"connections": "Webhook / CRM trigger → Normalize event → Idempotency lookup → Google Calendar freeBusy → Create or update event → Respond + audit"
}Пошаговая настройка связки ¶
- Создайте отдельный календарь или process calendar для продаж/записей, а не пишите сразу в личные календари менеджеров.
- Подключите Google Calendar credential в n8n и ограничьте доступ только нужными календарями.
- Перед созданием event добавьте freeBusy-проверку и ветку отказа, если слот занят.
- Сохраняйте Google event_id обратно в CRM, Postgres или таблицу mapping, чтобы повторный запуск обновлял встречу.
- В тестах отключите реальные приглашения или используйте test attendees, чтобы не отправить клиентам мусорные invite.
Что проверить после импорта workflow
Откройте каждую ноду, замените credentials и IDs, включите dry-run там, где доступно, затем выполните сценарий на тестовом объекте. Для write-действий добавьте отдельный флаг approval или manual step.
Тесты перед production ¶
Минимальный smoke test:
curl -X POST "https://YOUR-N8N-DOMAIN/webhook/google-calendar-booking-n8n" -H "Content-Type: application/json" --data @integration-google-calendar-n8n-booking-payload.json- тот же lead_id и slot повторно
- занятый слот в календаре
- другой timezone клиента
- пустой attendees
- ошибка OAuth или invalid calendar_id
Отдельно проверьте, что retry n8n не создаёт второй объект во внешнем сервисе. Для критичных действий используйте durable storage: Postgres, CRM custom field, Google Sheet mapping или другой слой с уникальным ключом.
Production-риски ¶
- Не сохраняется event_id — retry создаёт дубль встречи.
- Timezone берётся из сервера n8n, а не из входного контракта.
- sendUpdates включён в тестах и отправляет клиентам лишние приглашения.
- AI выбирает время без deterministic freeBusy-проверки.
- Recurring event изменяется целиком вместо одного instance.
Полезные ссылки и смежные материалы ¶
Внутренняя перелинковка помогает быстро перейти от общего integration-гайда к готовым workflow, а внешние ссылки ведут на официальную документацию API и n8n-нод.
Критерии готовности ¶
- Повторный payload не создаёт второй event.
- Занятый слот возвращает понятный отказ или альтернативы.
- event_id и idempotency_key сохраняются вне execution data.
- Есть отдельные правила для timezone, attendees, Meet и sendUpdates.
- Ошибки 401/403/429 уходят в alert или DLQ.