Если после заявки нужно дать человеку файл с датой встречи, необязательно сразу подключать Google Calendar или Outlook API. В n8n можно создать .ics — файл события iCalendar. Соберём учебную встречу, проверим её время и получим файл для скачивания.
Результат инструкции
Получим meeting-demo.ics: «Учебная встреча», 20 октября 2026 года с 10:00 до 10:30 UTC, описание и место. В файле будет один VEVENT с заданным UID. Никаких писем, приглашений участникам и записей в рабочий календарь схема не отправляет.
Файл можно проверить текстовым редактором, затем при необходимости импортировать в отдельный тестовый календарь. Импорт в конкретные календарные приложения редакция не тестировала, поэтому не обещаем одинаковое поведение Google Calendar, Outlook и Apple Calendar. Проверенный результат здесь — сам файл и его содержимое.
Что понадобится
- Отдельный новый workflow в n8n. Проверено на n8n 2.42.4, Convert to File 1.1 и Code 2.
- Для точного повторения — учебный Docker-экземпляр с TZ=UTC и GENERIC_TIMEZONE=UTC. В импортируемой схеме timezone тоже UTC.
- Вымышленные данные из статьи. Аккаунт календаря, SMTP и API-ключи для создания файла не нужны.
Не меняйте часовой пояс рабочего сервера ради эксперимента. Используйте отдельный учебный экземпляр. В другой среде сначала проверьте DTSTART и DTEND в файле: надпись UTC в настройке сама по себе не заменяет сверку результата.
Как установить n8n локально через Docker
Шаг 1. Импортируйте workflow
Скачать n8n-create-calendar-ics.json
Создайте новый workflow, выберите Import from File и загрузите JSON. Сохраните схему, но не публикуйте: используется ручной запуск. В ней пять узлов:
Manual Trigger → Sample Event → Check Event → Make ICS → Check File
Три узла Sample Event, Check Event и Check File — Code, JavaScript, Run Once for All Items. Make ICS — Convert to File. Оставьте названия как в образце: Check File обращается к Check Event по имени. Continue On Fail и Always Output Data не включайте.
Шаг 2. Задайте учебную встречу
В Sample Event вставьте:
return [{ json: {
"title": "Учебная встреча",
"start_utc": "2026-10-20T10:00:00.000Z",
"end_utc": "2026-10-20T10:30:00.000Z",
"description": "Вымышленная встреча для проверки n8n.",
"location": "Учебный кабинет",
"uid": "ezgpt-demo-meeting-001@example.invalid"
} }];
start_utc и end_utc — уже время UTC, а не местное время с добавленной буквой Z. Например, 10:00 UTC соответствует 13:00 при смещении UTC+3 и 15:00 при UTC+5. Присоединить Z к местному времени — значит изменить момент встречи.
UID в примере — искусственный идентификатор на example.invalid. Это не email получателя и не адрес, на который что-то будет отправлено. Для разных настоящих событий нужны разные постоянные UID; один учебный UID нельзя раздавать всем встречам.
Шаг 3. Проверьте входные поля
В Check Event вставьте код:
const items = $input.all();
if (items.length !== 1) throw new Error('one_event_required');
const e = items[0].json;
if (Object.keys(e).sort().join('|') !== 'description|end_utc|location|start_utc|title|uid') {
throw new Error('event_fields_invalid');
}
for (const key of ['title', 'description', 'location']) {
const max = key === 'description' ? 1200 : 80;
if (typeof e[key] !== 'string' || !e[key].trim() || e[key].length > max ||
!e[key].isWellFormed() || /[\u0000-\u0009\u000b-\u001f\u007f]/.test(e[key])) {
throw new Error('event_text_invalid');
}
if (key !== 'description' && e[key].includes('\n')) throw new Error('single_line_required');
}
for (const key of ['start_utc', 'end_utc']) {
const value = e[key];
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00\.000Z$/.test(value) ||
!Number.isFinite(Date.parse(value)) || new Date(value).toISOString() !== value) {
throw new Error('canonical_utc_required');
}
}
const minutes = (Date.parse(e.end_utc) - Date.parse(e.start_utc)) / 60000;
if (minutes <= 0 || minutes > 480) throw new Error('duration_invalid');
if (typeof e.uid !== 'string' || !/^ezgpt-demo-[a-z0-9-]{1,20}@example\.invalid$/.test(e.uid)) {
throw new Error('demo_uid_invalid');
}
return [{ json: e }];
Проверка принимает ровно одно событие с шестью известными полями. Даты должны быть каноническими строками UTC с .000Z и точностью до минуты. Конец — позже начала, длительность — не больше восьми часов. Строка без часового пояса, неверная календарная дата или отрицательная длительность останавливают схему.
Ограничения длины текста, формата UID и длительности выбраны для учебного примера. Это не полный набор правил iCalendar. Название и место должны быть однострочными; в описании разрешён перенос строки. Экранирование для самого .ics выполняет штатный генератор, а не этот Code.
Шаг 4. Настройте создание ICS
В Make ICS выберите Convert to File → Operation: Convert to ICS. Для полей с данными включите Expression и задайте:
| Поле | Значение | Что получится |
|---|---|---|
| Event Title | {{ $json.title }} | Название события |
| Start | {{ $json.start_utc }} | Начало в UTC |
| End | {{ $json.end_utc }} | Конец в UTC |
| Put Output File in Field | calendar | Имя поля binary с файлом |
All Day оставьте выключенным. Это получасовая встреча, не событие на весь день. Поля Start и End задайте оба: не оставляйте конец на автоматическое значение. В импортированном workflow выражения уже настроены, вручную вводить их повторно не нужно.
Имя calendar обозначает поле в binary n8n, не календарь пользователя и не папку на диске. Нативная операция создаёт настоящий файл, а на выходе JSON может быть пустым — это нормально.
Шаг 5. Добавьте описание, место и UID
В Options добавьте:
| Параметр | Значение | Назначение |
|---|---|---|
| File Name | meeting-demo.ics | Имя скачиваемого файла |
| Description | {{ $json.description }} | Описание из исходника |
| Location | {{ $json.location }} | Место из исходника |
| UID | {{ $json.uid }} | Постоянный идентификатор события |
Description, Location и UID тоже используют Expression; File Name — обычное фиксированное значение. Sequence оставьте 0, Status — Confirmed. Use Workflow Timezone оставьте выключенным: в этом примере вход уже задан в UTC.
Attendees, Organizer, Recurrence Rule и напоминания не добавляйте. Этот материал создаёт файл одной встречи, не сценарий приглашений или повторов. Статус Confirmed внутри файла не означает, что кто-либо принял приглашение.
В проверенной версии при Sequence: 0 генератор не вывел отдельную строку SEQUENCE. Это наблюдаемое поведение, не потеря времени события. Само наличие UID и начальной версии не гарантирует, что повторный ручной импорт обновит событие: некоторые приложения могут создать дубль.
Шаг 6. Проверьте созданный файл
В Check File вставьте:
const items = $input.all();
if (items.length !== 1 || Object.keys(items[0].binary ?? {}).join('|') !== 'calendar') {
throw new Error('one_calendar_file_required');
}
const item = items[0], meta = item.binary.calendar;
if (meta.fileName !== 'meeting-demo.ics' || meta.mimeType !== 'text/calendar') throw new Error('calendar_metadata_invalid');
const bytes = await this.helpers.getBinaryDataBuffer(0, 'calendar');
if (bytes.length < 1 || bytes.length > 10000) throw new Error('calendar_size_invalid');
const text = bytes.toString('utf8');
if (!Buffer.from(text, 'utf8').equals(bytes) || bytes[0] === 239) throw new Error('calendar_utf8_invalid');
const e = $('Check Event').first().json;
// Unfold content lines only for these checks, not a general iCalendar parser.
const lines = text.replace(/\r\n[ \t]/g, '').split('\r\n');
const stamp = value => value.replaceAll('-', '').replaceAll(':', '').replace('.000', '');
for (const line of ['BEGIN:VCALENDAR', 'END:VCALENDAR', 'BEGIN:VEVENT', 'END:VEVENT',
'UID:' + e.uid, 'DTSTART:' + stamp(e.start_utc), 'DTEND:' + stamp(e.end_utc)]) {
if (lines.filter(value => value === line).length !== 1) throw new Error('calendar_marker_mismatch');
}
if (lines.some(line => /^(ATTENDEE|ORGANIZER|RRULE)(;|:)|^BEGIN:VALARM$/.test(line))) {
throw new Error('unexpected_calendar_action');
}
return [{ json: {
file_name: meta.fileName, start_utc: e.start_utc, end_utc: e.end_utc,
uid: e.uid, verified: true, action_performed: false, ics_text: text
}, binary: item.binary }];
Код читает настоящий binary.calendar через getBinaryDataBuffer, проверяет имя meeting-demo.ics, MIME text/calendar и UTF-8. Затем сверяет известные строки начала, конца и UID, наличие одного блока события и отсутствие участников, организатора, повторов и напоминаний.
Это ограниченная проверка файла, созданного нашей схемой, не универсальный iCalendar-парсер. Она не подтверждает все поля по смыслу и не заменяет проверку стороннего .ics библиотекой. Отдельно редакция разобрала успешные тестовые файлы библиотекой icalendar и сверила название, описание, место, UID и даты.
В ICS длинная строка может переноситься с продолжением на следующей строке. Код убирает такое продолжение только для своих контрольных проверок. Не редактируйте переносы и экранирование вручную: запятые, обратные слеши и переносы в описании должен записать генератор формата.
Шаг 7. Выполните всю цепочку
Нажмите Execute workflow от Manual Trigger. Должны успешно завершиться все пять узлов. В JSON последнего ожидаются следующие поля:
{
"file_name": "meeting-demo.ics",
"start_utc": "2026-10-20T10:00:00.000Z",
"end_utc": "2026-10-20T10:30:00.000Z",
"uid": "ezgpt-demo-meeting-001@example.invalid",
"verified": true,
"action_performed": false
}
Кроме них будет ics_text — полный текст файла. binary.calendar остаётся на выходе для скачивания. verified: true означает, что прошли проверки Check File; action_performed: false — что схема не отправляла приглашение и не выполняла запись в календарь.
Если Check Event остановился, Make ICS не должен создавать файл. Если Check File остановился после генерации, не используйте файл из промежуточного узла как проверенный: сначала исправьте причину и повторите полный запуск.
Шаг 8. Скачайте и откройте файл как текст
В Check File откройте вкладку Binary, выберите calendar и скачайте meeting-demo.ics. Сначала откройте его именно текстовым редактором, а не двойным щелчком: по ассоциации .ics может запуститься календарь.
Внутри ожидаются BEGIN:VCALENDAR, один BEGIN:VEVENT, затем DTSTART:20261020T100000Z и DTEND:20261020T103000Z. UID должен совпасть с Sample Event. SUMMARY содержит название, DESCRIPTION — описание, LOCATION — место. DTSTAMP отражает время создания файла, поэтому между запусками может меняться.
Образец получен реальным запуском схемы. Он содержит вымышленную встречу на указанную дату, не актуальное приглашение. После скачивания браузер или система могут предложить импорт — для просмотра структуры это не требуется.
Шаг 9. Сверьте время до импорта
Z в DTSTART и DTEND означает UTC. Если ваш календарь показывает местное время, отображаемые часы могут отличаться от 10:00: так и должно быть при другом часовом поясе. Сверьте именно один и тот же момент, а не одинаковые цифры на часах.
Для дополнительной проверки можно импортировать файл в отдельный пустой тестовый календарь, затем сравнить дату и получасовую длительность. Не импортируйте учебный образец в общий рабочий календарь и не рассчитывайте, что повторный импорт будет автоматически безопасен от дублей. Этот шаг зависит от вашего приложения и отдельно редакцией не проверялся.
Как привести дату из заявки к UTC в n8n
Шаг 10. Проверьте отказ на ошибочных данных
В Sample Event временно поставьте end_utc равным start_utc и запустите всю цепочку. Check Event должен остановиться с duration_invalid, файл не создаётся. Затем удалите Z из начала: ожидается canonical_utc_required. Верните исходные значения и убедитесь, что Check File снова выдаёт verified: true.
Ноль items на выходе Sample Event не означает создание пустого календаря: дальнейшие узлы не выполнятся и файла не будет. Несколько событий эта схема тоже намеренно не принимает. Для массовой выгрузки и нескольких VEVENT внутри одного файла нужен другой сценарий.
Если результат отличается
| Симптом | Причина | Что сделать |
|---|---|---|
| canonical_utc_required | Неверная дата или не UTC | Вернуть канонический формат .000Z |
| duration_invalid | Конец не позже начала или больше 8 часов | Исправить интервал |
| Нет binary.calendar | Неверная операция или имя поля | Проверить Convert to ICS и calendar |
| calendar_marker_mismatch | Изменились даты, UID или All Day | Сверить настройки и часовой пояс |
| В календаре появились дубли | Повторный импорт файла | Проверять импорт в отдельном календаре |
Перед подключением к заявкам
Сначала приведите даты из формы или CRM к UTC и определите постоянный UID для каждой встречи. Не используйте номер телефона или email как публичный идентификатор. Не добавляйте автоматическую отправку до согласования адресата, текста приглашения и правил обновления.
Создание .ics не проверяет свободные слоты, не бронирует комнату, не подтверждает участие и не синхронизируется с календарём. Для этого нужны соответствующие API и отдельные проверки. Учитывайте, что текст файла и данные встречи могут сохраняться в истории executions n8n.
Как вернуть файл из Webhook в n8n
Что проверено редакцией
18 октября 2026 года схема проверена на n8n 2.42.4 в отдельном Docker-экземпляре, процесс и workflow в UTC, binary на диске. Все 33 сценария прошли: точный публичный workflow, Unicode, спецсимволы и перенос в описании, длинное описание с переносами iCalendar, длительность 1 минута и 8 часов, переход через полночь; workflow timezone Asia/Yekaterinburg с выключенным и включённым Use Workflow Timezone в этой UTC-среде; пропуски и лишние поля, пустой и слишком длинный текст, управляющие символы и непарный суррогат, неверная дата, отсутствие зоны, смещение вместо канонического UTC, секунды, нулевая, отрицательная и слишком большая длительность, неверный UID; имя, binary-поле, изменённое время, All Day, автоматический UID и добавление участника. Два входных события отклонены, пустой источник не создал файл. Все девять успешных вариантов независимо разобраны icalendar 7.3.0: название, описание, место, UID, начало и конец совпали, нет участников, организатора, повторов и напоминаний. Sequence 0 не попал в выходной ICS, проверка это учитывает. Файл создан штатным Convert to File, не ручной склейкой. Контейнер остановлен, существующие workflows не менялись. Клики редактора, ручное скачивание из n8n, импорт в приложения, доставка приглашений, Cloud и другие часовые пояса процесса не тестировались. Обложка — AI-иллюстрация.
Частые вопросы
Нужен доступ к Google Calendar или Outlook?
Для создания .ics нет. Схема делает файл внутри n8n без календарных credentials. Запись в календарь и отправка приглашений — другие действия.
Почему в календаре время не 10:00?
В примере 10:00 UTC. Календарь может показать тот же момент в местном часовом поясе: например, 13:00 при UTC+3. Проверьте зону, дату и длительность.
Status: Confirmed означает, что участник согласился?
Нет. Это статус события в файле. В нашей схеме участников вообще нет, и никакого ответа от них не получено.
UID защитит от дублей при повторном импорте?
Гарантировать это нельзя. UID идентифицирует событие, но поведение импорта и обновления зависит от приложения и сценария. Сначала проверяйте в отдельном календаре.
Можно создать событие на весь день этой схемой?
Это другой режим. Здесь All Day выключен, проверяется точное UTC-время. Для событий на весь день нужны отдельные правила дат и окончания; простое переключение не пройдёт проверку.