n8n Schedule Trigger 时区设置与错误触发时间
n8n 调度会读取 TZ、GENERIC_TIMEZONE 和工作流时区。了解默认 America/New_York 的版本陷阱,并修复计划触发提前或延后数小时的问题。
为什么 n8n 的计划触发器会在错误的时间触发
n8n 的计划触发器会在错误的时间触发,因为 n8n 会从三个不同位置读取时区设置,而修复其中一个位置只能解决部分问题。这三个位置分别是容器自身的 TZ 变量、实例默认的 GENERIC_TIMEZONE,以及单个工作流内部设置的时区。一次性设置好这三项后,之后创建的每个计划都会按预期执行。
先纠正一个常见猜测。全新部署的自托管 n8n 不会按 UTC(协调世界时)进行计划调度。容器时钟使用 UTC,因为官方镜像未设置 TZ。计划调度是单独的一层,而 n8n 文档中 GENERIC_TIMEZONE 的默认值为 America/New_York(截至 August 2026),因此未修改的实例会按照纽约时间触发 Schedule Trigger。这就是为什么用户报告的时差通常与其所在位置相对于 UTC 的实际时差不一致。位于 Berlin 的用户设置为 06:00 后,本地时间会在 12:00 触发;而在 March 的那几周,美国已经进入夏令时、Europe 尚未进入时,触发时间会是 11:00。
三个时区层级,以及哪个优先
TZ 是容器内的操作系统时区。n8n 文档将其描述为用于设置系统时区的变量,以控制脚本以及 date 等命令返回的结果。它决定容器内 date 的输出、容器日志行记录的时间戳、Code 节点中 new Date() 的返回值,以及您在容器内运行的任何 shell 脚本所读取的时区。它不会影响 Schedule Trigger 触发的时间。
GENERIC_TIMEZONE 是 n8n 实例时区。文档称其为 n8n 实例时区,并指出它对 Cron 等调度节点很重要。这里的 Cron 指标准的基于时间的调度语法,n8n 将其作为 Schedule Trigger 中的 Custom (Cron) 选项提供。
工作流时区按工作流单独设置。在画布中打开工作流,选择右上角的三个点,选择 Settings,然后修改 Timezone 值。它会针对该工作流覆盖 GENERIC_TIMEZONE。本页的所有设置都属于免费自托管版本,因此正确配置调度不依赖付费计划;确实需要许可证密钥的功能完全是另一组功能。
对于 Schedule Trigger,优先级顺序是固定的。如果工作流设置了时区,n8n 会使用工作流时区;否则使用 GENERIC_TIMEZONE 中的实例时区;如果两者都未设置,则使用内置默认值 America/New_York。在这一决策的任何步骤中,都不会读取 TZ。
对于节点内部的日期,结果取决于代码请求的是哪个时钟。Luxon 是 n8n 表达式使用的日期库,它采用 n8n 时区,因此 $now 和 $today 遵循与触发器相同的“工作流优先,否则实例”顺序。Code 节点中的原生 JavaScript new Date() 会读取操作系统时区,因此遵循 TZ。这正是大多数混淆的来源:触发器可能运行正确,但工作流写入的每个时间戳都可能相差数小时。
在 Compose 文件中同时设置这三项
将 TZ 和 GENERIC_TIMEZONE 放在文件中相邻的位置,避免只设置其中一项而遗漏另一项。下面的片段是一个可正常运行的服务中与时区相关的部分。文件的其余部分,包括反向代理和证书配置,参见在 VPS 上通过 HTTPS 部署自托管 n8n。
services:
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- GENERIC_TIMEZONE=Europe/Berlin
- TZ=Europe/Berlin
- N8N_RUNNERS_ENABLED=true
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:使用 docker compose up -d 应用更改,不要使用 docker compose restart。重启只会使用创建容器时的环境变量再次启动同一个容器,因此文件已更改,但运行中的进程不会更新。up -d 会检测到环境变量已更改,并重新创建容器。如果您将这些值放在 env 文件中,而不是直接写入配置,仍然适用同样的重新创建规则;Compose env 文件和密钥处理指南介绍了该文件的读取位置。
在 Region/City 中使用 IANA(Internet Assigned Numbers Authority)时区名称,例如 Europe/Berlin 或 America/Sao_Paulo。这些名称包含相应地区的夏令时规则,因此当地时钟变化时,UTC 偏移量也会随之变化。像 Etc/GMT+5 这样的固定偏移名称不会随季节变化,而且其符号方向与通常的直觉相反。运行 LC_ALL=C TZ=Etc/GMT+5 date +%z,它会输出 -0500。请避免使用这些名称。
只设置其中一个会导致修复不完整
只设置 GENERIC_TIMEZONE 时,Schedule Trigger 会在您指定的小时触发,但所有读取操作系统时间的组件仍使用 UTC。调用 new Date().toString() 的 Code 节点会返回 UTC 字符串,容器日志行会记录为 UTC,使用系统时钟生成的文件名也会在错误的午夜切换日期。
只设置 TZ 时,情况正好相反。docker compose exec n8n date 会输出本地时间,看起来配置已生效,但 Schedule Trigger 仍使用 America/New_York,并在与您指定时间相差 6 小时的时刻触发。这种情况最浪费排查时间,因为大多数人首先执行的检查此时会通过。
先设置工作流时区,之后再修改 GENERIC_TIMEZONE 时,该工作流不会采用此更改。工作流中的时区设置优先,并会持续生效,直到有人打开该工作流的设置。某个工作流在异常时间运行,而相邻工作流均正常时,几乎总是由此导致。
直接检查时钟,不要靠猜测
直接比较主机和容器的时间。
date
docker compose exec n8n date
docker compose exec n8n printenv TZ GENERIC_TIMEZONE设置 TZ 后,前两个命令应打印相同的本地时间。printenv 会为每个已存在的变量打印一行,因此输出两行表示两个变量都已设置,输出一行表示当前仍处于只修复了一半的状态。
现在从工作流内部询问 n8n 本身,因为容器 shell 无法告诉您工作流级别的时区是什么。在出现问题的工作流中添加一个 Code 节点,然后使用 Execute Workflow 运行一次。
return [
{
json: {
n8n_time: $now.toISO(),
n8n_zone: $now.zoneName,
system_time: new Date().toString(),
},
},
];n8n_zone 是此工作流的 Schedule Trigger 将使用的时区。它已经按照先工作流、后实例的顺序完成解析,因此可以直接回答这个问题。system_time 携带容器自身从 TZ 获取的时区。在出现问题的工作流中运行它,不要在新建的工作流中运行,因为工作流级别的设置会随工作流保存。如果这两个值不一致,您无需打开任何配置文件即可找到问题。
Schedule Trigger 节点中的 Cron 表达式
Schedule Trigger 提供从秒到月的固定间隔,也提供 Custom (Cron) 选项,用于处理这些间隔无法覆盖的情况。Cron 表达式会按照工作流解析后的时区读取,因此 0 6 * * * 表示该时区的 06:00,而不是 UTC 的 06:00。来自 crontab guru 的五字段表达式可以直接粘贴使用。n8n 还接受可选的秒字段。文档中的字段表将其放在第一位:秒、分钟、小时、日期、月份、星期。
不要手动编码时区偏移。在 UTC 实例上写入 0 4 * * * 以实现柏林时间 06:00,只在冬季正确,整个夏季都会早 1 小时,因为柏林冬季为 UTC+1,夏季为 UTC+2。设置时区,并填写您实际需要的本地小时。
夏令时对设置为 02:30 的任务有什么影响
本地墙上时钟时间并不保证对应某个确定时刻。每年有两次会少一个小时,也会重复一个小时,因此安排在这些时段内的任务都会受到影响。无需使用 n8n,在任何 Linux 主机上通过 date 即可观察这一现象。
LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30'date: invalid date '2027-03-28 02:30'命令没有拼写错误。2027-03-28,柏林时间会从 02:00 直接跳到 03:00,因此当天不存在本地时间 02:30,date 无法将其转换为确定时刻。绑定到本地时间 02:30 的任务没有可执行的时刻。相邻时间则没有问题:date -d '2027-03-28 01:30' 会解析为 CET,date -d '2027-03-28 03:30' 会解析为 CEST。
秋季转换正好相反。2027-10-31,柏林时间会从 03:00 回拨到 02:00,因此 02:30 会出现两次。
LC_ALL=C TZ=Europe/Berlin date -d '2027-10-31 02:30 CEST' '+%s'
LC_ALL=C TZ=Europe/Berlin date -d '2027-10-31 02:30 CET' '+%s'1824942600
1824946200这是两个不同的时刻,但它们都称为本地时间 02:30,相隔 3600 秒。固定在该时间的任务可能运行两次,也可能在无人指定的时间运行一次。对于计费任务或备份轮换,这两种结果都不可接受。请将计划移出该时间窗口。在大多数欧洲和北美时区,风险时段是本地时间 00:00 至 03:00。
使用 UTC 调度基础设施,并向用户显示本地时间
标准做法是区分时区的两项职责。机器需要稳定的时间间隔。用户需要易读的时间。
- 对于无人查看的工作,将工作流时区设为 UTC。备份、缓存预热、日志传输和报告生成都属于此类。在 UTC 下,全年每天两次运行之间的间隔都与配置的间隔完全一致,因为 UTC 没有夏令时。
- 对于需要人工查看的工作,仍使用 UTC 设置调度,并在显示时转换时间。一个表达式即可完成:
{{ $now.setZone('Europe/Berlin').toFormat('yyyy-MM-dd HH:mm') }}会将本地时间写入消息正文,同时保持触发时间稳定。
同样的划分也适用于 n8n 之外的场景。当部分自动化任务在 VPS 上作为 systemd 服务和计时器运行时,其中的 OnCalendar 行会使用系统时区解析。系统时区是另一个独立设置的时钟。将所有调度器都设为 UTC 后,只需记住一条规则,而不是四条。对于汇总某个时间段的任务,这一点也很重要,因为要求处理前一天数据的 n8n AI 代理工作流会根据解析时使用的时区,静默地采用不同的 24 小时范围。
故障模式及您将看到的输出
所有时间都相差约六小时。 未设置 GENERIC_TIMEZONE,因此使用内置默认值 America/New_York。docker compose exec n8n printenv GENERIC_TIMEZONE完全没有输出。设置该变量,然后重新创建容器。
您修改了 Compose 文件,但没有任何变化。 您运行了 docker compose restart,因此容器继续使用原有环境变量。运行 docker compose up -d,然后使用 docker compose exec n8n printenv TZ 确认。
触发器正确,但时间戳错误。 只设置了 GENERIC_TIMEZONE。Code 节点中的 new Date() 仍会从操作系统读取 UTC。将 TZ 设置为相同的值,然后重新创建容器。
某个工作流忽略了实例设置。 该工作流在自身设置中指定了时区,其优先级高于 GENERIC_TIMEZONE。打开画布,依次选择三个点、Settings、Timezone。
某个每日任务今年曾执行两次,或漏执行一天。 其计划执行时间处于夏令时切换期间。调整执行时间,或将该工作流改为使用 UTC。
FAQ
为什么我的 n8n 计划触发器会在错误的时间触发?
工作流解析出的时区与您假定的不同。如果工作流设置了时区,n8n 会使用该时区;否则使用 GENERIC_TIMEZONE 中的实例时区;如果两者都未设置,则使用内置默认值 America/New_York。未设置 GENERIC_TIMEZONE 的自托管实例会按纽约时间安排任务,而不是按 UTC 安排,因此时间偏移通常与您所在时区相对于 UTC 的偏移不一致。运行 docker compose exec n8n printenv GENERIC_TIMEZONE。没有输出表示该变量从未设置。
n8n 中的 TZ 与 GENERIC_TIMEZONE 有什么区别?
TZ 是容器内的操作系统时区。它决定容器内 date 的返回值、容器日志行中的时间戳、Code 节点中 new Date() 的返回值,以及您在容器内运行的任何脚本所看到的时区。GENERIC_TIMEZONE 是 n8n 实例时区,计划节点和 Luxon 表达式(例如 $now)会使用它。只设置其中一个,会导致触发时间正确但时间戳错误,或时间戳正确但触发时间相差数小时。请将两者设置为相同的值。
应该设置工作流时区,还是设置 GENERIC_TIMEZONE?
将 GENERIC_TIMEZONE 设置为整个实例的默认时区。只有在某个工作流确实属于其他时区时,才使用工作流级别的设置。工作流时区会覆盖实例时区,也不会随之后对 GENERIC_TIMEZONE 的修改而变化。因此,遗留的工作流级别覆盖设置可能在数月后仍难以追踪。
在时钟切换时,安排在 02:30 运行的任务会怎样?
该本地时间可能会消失,也可能出现两次。LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30' 返回 date: invalid date '2027-03-28 02:30',因为柏林时间当天会从 02:00 直接跳到 03:00。2027-10-31,同一个墙上时钟时间会对应相隔一小时的两个时刻。请避免将计划任务安排在本地时间 00:00 至 03:00 之间,或将工作流设置为 UTC,仅在人员查看时间时再转换为本地时间。