Клиент выбрал 16 октября в 00:15 по Москве, а в системе нужно сохранить точный момент в UTC. Это будет ещё 15 октября, 21:15. Простая замена точек на дефисы такого преобразования не делает. Настроим n8n так, чтобы он явно понимал исходный часовой пояс, проверял дату и выдавал строку для дальнейшей передачи в API.
Результат инструкции
Соберём ручной workflow из пяти узлов. На входе три вымышленные заявки со строкой local_time в формате dd.MM.yyyy HH:mm. Все значения по договорённости относятся к Europe/Moscow. Luxon проверит календарную дату и добавит смещение, штатный Date & Time сформирует scheduled_at_utc, а последний Code сверит результат с исходным моментом времени.
На выходе останутся три items с ID, исходной датой, зоной и UTC-строкой вида 2026-10-15T06:30:00+00:00. Ничего не бронируется и не отправляется: status ready_for_review, action_performed false. Это подготовка данных, не календарь, таймер и не AI-агент.
Что понадобится
- Работающий n8n и права создать отдельный workflow. Проверено на n8n 2.42.4, Date & Time версии 2.
- Вымышленные даты из примера. Календарь, API-ключ и внешняя база не нужны.
- Согласованное правило входа: точность до минуты, годы 2000–2099, исходная зона Europe/Moscow.
Установить n8n локально через Docker
Шаг 1. Импортируйте учебную схему
Создайте новый workflow и выберите Import from File в меню редактора. Загрузите JSON. Если браузер открыл текст файла, сохраните его и импортируйте. На полотне появится цепочка:
Manual Trigger → Sample Requests → Parse Local Time → Format UTC → Check Result
Format UTC — Date & Time. Sample Requests, Parse Local Time и Check Result — Code. При ручной сборке задайте именно эти имена и последовательные соединения. В файле нет credentials и ID другого workflow. Он неактивен; для ручного выполнения публикация не требуется.
Шаг 2. Задайте часовой пояс workflow
Откройте Settings самого workflow, не отдельного узла. В Timezone выберите UTC и сохраните. В импортированном файле это уже задано. Настройку всего экземпляра n8n менять не нужно: остальные workflows могут использовать свои зоны.
Исходная зона клиента и зона workflow здесь разные. Europe/Moscow отвечает на вопрос, что означает введённое 09:30. UTC нужен для единого выходного представления. Не выбирайте зону по текущим часам своего компьютера и не вычитайте три часа вручную.
Шаг 3. Подготовьте три входные даты
В Sample Requests задайте Mode: Run Once for All Items и Language: JavaScript. Вставьте:
const requests = [
{
"request_id": "T1",
"local_time": "15.10.2026 09:30"
},
{
"request_id": "T2",
"local_time": "16.10.2026 00:15"
},
{
"request_id": "T3",
"local_time": "17.10.2026 18:00"
}
];
if (!requests.length) throw new Error("requests_empty");
return requests.map(json => ({ json }));
Выполните источник от Manual Trigger. Должно быть три items с T1/T2/T3. local_time остаётся строкой с ведущими нулями: две цифры дня, две месяца, четыре года и время HH:mm. T2 специально находится около полуночи, чтобы проверить изменение календарного дня.
Пустой список requests в этом источнике вызывает requests_empty. Это наше правило теста, не встроенная ошибка Date & Time. Если в вашей задаче пустой вход допустим, предусмотрите отдельный результат no_data, а не фиктивную дату.
Шаг 4. Разберите дату с явной исходной зоной
В Parse Local Time задайте Run Once for All Items и JavaScript. Вставьте:
const FORMAT = 'dd.MM.yyyy HH:mm';
const ZONE = 'Europe/Moscow';
const ids = new Set();
return $input.all().map((item, i) => {
const row = item.json;
if (typeof row.request_id !== 'string' || !row.request_id.trim() || ids.has(row.request_id)) {
throw new Error('request_id_missing_or_duplicate');
}
if (typeof row.local_time !== 'string' ||
!/^\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}$/.test(row.local_time)) {
throw new Error('local_time_format_invalid');
}
const parsed = DateTime.fromFormat(row.local_time, FORMAT, { zone: ZONE });
if (!parsed.isValid || parsed.year < 2000 || parsed.year > 2099 ||
parsed.toFormat(FORMAT) !== row.local_time) throw new Error('local_time_invalid');
ids.add(row.request_id);
return { json: {
request_id: row.request_id,
local_time: row.local_time,
source_zone: ZONE,
source_iso: parsed.toISO(),
}, pairedItem: { item: i } };
});
Используется встроенный в JavaScript-среду n8n DateTime из Luxon. Устанавливать пакет или разрешать внешние модули для этого примера не нужно. FORMAT задаёт порядок частей даты, ZONE — её исходный смысл. Проверка isValid отклоняет невозможный день, а обратное форматирование замечает нормализацию вроде перехода 24:00 на следующий день.
У T1 в Output Parse Local Time должен появиться source_iso 2026-10-15T09:30:00.000+03:00. Это уже однозначный момент времени со смещением. Исходная local_time тоже сохраняется. Не передавайте дальше сам объект DateTime: между узлами мы используем обычные строки.
Проверка принимает строго указанный формат и годы 2000–2099. null, пустота, лишние пробелы, другая запись даты и повтор ID останавливают запуск. Мы намеренно не угадываем, что хотел сказать источник.
Шаг 5. Настройте Date & Time
Откройте Format UTC. В Operation выберите Format a Date, в Date включите Expression:
{{ $json.source_iso }}
Далее задайте параметры из таблицы. Поля Format, Custom Format и Output Field Name вводятся как настройки, не как выражения:
| Параметр | Значение | Зачем |
|---|---|---|
| Operation | Format a Date | Форматирование момента |
| Date | Expression: $json.source_iso | ISO с исходным смещением |
| Format | Custom Format | Свой шаблон результата |
| Custom Format | yyyy-MM-dd'T'HH:mm:ssZZ | Дата, время и фактическое смещение |
| Output Field Name | scheduled_at_utc | Отдельное выходное поле |
В Custom Format вставьте шаблон целиком:
yyyy-MM-dd'T'HH:mm:ssZZ
Регистр значим: MM — месяц, mm — минуты. Буква T в одинарных кавычках — буквальный разделитель, ZZ добавляет смещение с двоеточием. Не заменяйте ZZ на текст Z в кавычках: так можно подписать не-UTC время как UTC, не выполнив преобразование.
Шаг 6. Сохраните исходные поля и включите зону workflow
В Options узла Format UTC добавьте Include Input Fields и включите его. Добавьте Use Workflow Timezone и тоже включите. Поэтому штатный узел оформит уже определённый момент в зоне UTC из шага 2, а не оставит исходное представление.
From Date Format здесь не задавайте: на вход этого узла уже приходит ISO со смещением, не исходная строка 15.10.2026 09:30. Разбор исходной строки сделан в Parse Local Time с явно указанной зоной. Это не опора на автоматическое распознавание русского формата или системную зону сервера.
В Settings узлов оставьте On Error: Stop Workflow. Execute Once, Always Output Data и Retry On Fail не включайте. Повтор не исправит 31 февраля.
Шаг 7. Добавьте проверку UTC и исходного момента
В Check Result выберите Run Once for All Items, JavaScript и вставьте весь код:
Показать код (32 строк)
const items = $input.all();
const source = $('Parse Local Time').all().map(item => item.json);
if (items.length !== source.length) throw new Error('date_count_mismatch');
return items.map((item, i) => {
const row = item.json;
const original = source[i];
const keys = Object.keys(row).sort().join('|');
if (keys !== 'local_time|request_id|scheduled_at_utc|source_iso|source_zone') {
throw new Error('date_fields_mismatch');
}
for (const key of ['request_id', 'local_time', 'source_zone', 'source_iso']) {
if (row[key] !== original[key]) throw new Error('date_source_mismatch');
}
if (typeof row.scheduled_at_utc !== 'string' ||
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$/.test(row.scheduled_at_utc)) {
throw new Error('utc_format_invalid');
}
const utc = DateTime.fromISO(row.scheduled_at_utc, { setZone: true });
const input = DateTime.fromISO(original.source_iso, { setZone: true });
if (!utc.isValid || utc.offset !== 0 || utc.toMillis() !== input.toMillis() ||
utc.setZone(original.source_zone).toFormat('dd.MM.yyyy HH:mm') !== original.local_time) {
throw new Error('date_instant_mismatch');
}
return { json: {
request_id: row.request_id,
local_time: row.local_time,
source_zone: row.source_zone,
scheduled_at_utc: row.scheduled_at_utc,
status: 'ready_for_review',
action_performed: false,
}, pairedItem: { item: i } };
});
Проверка требует тот же набор записей и исходных полей, UTC-смещение +00:00 и совпадение момента времени через toMillis. Дополнительно результат переводится обратно в Europe/Moscow и сравнивается с исходной строкой. Так обнаруживается не только другой формат, но и тихий сдвиг времени.
Сопоставление по позиции рассчитано на эту последовательную цепочку без сортировки, фильтров и Merge между узлами. Если измените маршрут, пересмотрите проверку. Источник и проверка не должны связывать чужие заявки только потому, что даты похожи.
Шаг 8. Выполните весь workflow
Нажмите Execute Workflow. В Format UTC должно быть три items. В Check Result тоже три. Первый ожидаемый результат:
{
"request_id": "T1",
"local_time": "15.10.2026 09:30",
"source_zone": "Europe/Moscow",
"scheduled_at_utc": "2026-10-15T06:30:00+00:00",
"status": "ready_for_review",
"action_performed": false
}
Строка +00:00 означает UTC. Запись с Z может обозначать тот же момент, но в этом примере выбран один точный выходной формат с +00:00, и проверка требует именно его. Строки не нужно сравнивать как произвольный текст, если задача — выяснить, относятся ли они к одному моменту.
Шаг 9. Сверьте переход через полночь
| Заявка | Вход Europe/Moscow | Ожидаемый UTC |
|---|---|---|
| T1 | 15.10.2026 09:30 | 2026-10-15T06:30:00+00:00 |
| T2 | 16.10.2026 00:15 | 2026-10-15T21:15:00+00:00 |
| T3 | 17.10.2026 18:00 | 2026-10-17T15:00:00+00:00 |
У T2 день на выходе стал 15 октября вместо 16-го. Это не потеря суток, а другой часовой пояс того же момента. Если ваша задача хранит просто календарную дату без времени, например день рождения, не применяйте эту схему автоматически: у такого значения иной смысл.
Для дополнительного опыта в отдельной копии оставьте одну заявку с 01.01.2027 00:15. Ожидается 2026-12-31T21:15:00+00:00. Такая проверка ловит ошибки не только времени, но и месяца и года.
Шаг 10. Проверьте невозможные даты
В Sample Requests временно замените дату T1 на 31.02.2026 09:30. Запуск должен остановиться в Parse Local Time с local_time_invalid, до успешного результата Check Result. Так же должны отклоняться 29.02.2026 09:30, 15.10.2026 24:00 и 15.10.2026 09:60.
На отдельном опыте 29.02.2024 09:30 должен пройти: ожидаемый UTC 2024-02-29T06:30:00+00:00. Строка 2026-10-15 09:30 должна дать local_time_format_invalid, потому что не соответствует договорённому формату, хотя сама дата существует. Верните три исходные заявки.
Шаг 11. Убедитесь, что неверная настройка замечена
На учебной копии поменяйте Timezone workflow с UTC на Europe/Moscow и выполните всю схему. Format UTC вернёт представление с +03:00, а Check Result остановится с utc_format_invalid. Имя поля scheduled_at_utc само по себе не делает значение UTC. Верните UTC.
Теперь выключите Include Input Fields в Format UTC. Проверка должна дать date_fields_mismatch: пропали ID и исходная дата. Верните опцию. Если вместо Expression подставить одну фиксированную дату для всех заявок, Check Result заметит date_instant_mismatch, когда дойдёт до другой заявки. Не отключайте проверку ради зелёного результата.
Шаг 12. Восстановите и сохраните результат
Верните исходные три записи, Timezone UTC, Date через source_iso, полный custom format и обе опции из шага 6. Сохраните и выполните весь workflow. Сверьте каждую строку с таблицей, особенно T2. У всех должны быть status ready_for_review и action_performed false.
Готово: данные подготовлены, но события календаря не созданы. Перед настоящим API выясните, принимает ли он ISO со смещением, UTC с Z или отдельную зону, и проверьте тестовый запрос. Не передавайте время без зоны как будто это UTC и не меняйте формат исходного значения молча.
Если результат отличается
| Симптом | Причина | Что проверить |
|---|---|---|
| local_time_format_invalid | Вход не по шаблону | dd.MM.yyyy HH:mm без пробелов по краям |
| local_time_invalid | Дата невозможна или вне диапазона | Календарный день, время, год |
| utc_format_invalid | Не тот пояс или выходной формат | Workflow UTC, ZZ и полное время |
| date_fields_mismatch | Потеряны поля или другое имя даты | Include Input Fields, scheduled_at_utc |
| date_instant_mismatch | Момент не совпал с исходным | Expression в Date и маршрут данных |
Где заканчивается эта схема
Europe/Moscow здесь — правило для всех заявок, не вывод по IP, языку или номеру телефона клиента. Для нескольких зон нужно получать достоверную зону каждой заявки и отдельно проектировать проверку. Переходы летнего времени, повторяющиеся и несуществующие локальные часы в других зонах в этом материале не тестировались. Не переносите пример на них без дополнительных правил.
Это преобразование не ставит workflow на ожидание и не запускает его по расписанию. Оно также не решает, допустима ли дата в прошлом. Такие правила зависят от задачи и добавляются отдельно.
Подготовить поля для API через Edit Fields
Остановить workflow при нарушении договора данных
Что проверено редакцией
15 октября 2026 года workflow проверен на n8n 2.42.4 в отдельном Docker-экземпляре штатными ручными запусками API. Все 24 проверки прошли: три даты, одна запись, обратный порядок, лишнее поле, 29 февраля високосного года, переход через Новый год, пустой источник, 31 февраля, 29 февраля невисокосного года, 24:00, минуты 60, месяц 13, другой формат, null/отсутствующая/пустая дата, внешние пробелы, повтор/отсутствие ID, неверный timezone workflow, выключенный перенос входных полей, другое имя результата, формат только даты и фиксированный вход вместо выражения. Сверены точные UTC-строки, количество, исходные поля и отсутствие успешного результата при ошибках. Luxon разбирает вход с явной Europe/Moscow, штатный Date & Time версии 2 формирует выход с зоной workflow UTC. Официальные документы n8n, Luxon и код Date & Time сверены. Редакторские клики, Cloud, другие зоны и переходы летнего времени, автоматические триггеры, большие объёмы и внешние API не тестировались. Учебный контейнер остановлен, чужие workflows не менялись. Обложка — редакционная AI-иллюстрация.
Частые вопросы
Почему UTC-дата стала на день раньше?
Локальная полночь и UTC относятся к разным показаниям часов. В примере 16 октября 00:15 Europe/Moscow соответствует 15 октября 21:15 UTC. Обратная проверка подтверждает тот же момент.
Можно просто вычесть три часа?
В примере преобразование выполняет библиотека по явно заданной зоне. Ручной сдвиг легко применить дважды или перенести на другую зону неверно. Не меняйте цифры часов без учёта смысла исходного времени.
Почему используется +00:00, а не Z?
Оба обозначения могут выражать UTC. Здесь выбран конкретный формат с +00:00. Если API требует Z, измените форматирование и проверку согласованно, а не дописывайте Z к локальному времени.
Что делать, если заявки приходят из разных зон?
Нужна достоверная зона на каждую заявку и отдельные правила её проверки. Эта схема рассчитана на фиксированную Europe/Moscow и не проверяет неоднозначные часы переходов летнего времени.
Почему после преобразования не создалась встреча?
Схема только подготавливает и проверяет строки даты. Узла календаря и внешнего действия в ней нет, поэтому action_performed false. Подключение API требует отдельного теста.