n8n : corriger l’heure d’un déclencheur Schedule
Un Schedule Trigger n8n peut démarrer à la mauvaise heure : vérifiez TZ, GENERIC_TIMEZONE et le fuseau du workflow pour éviter les décalages.
Pourquoi votre déclencheur Schedule n8n s’exécute à la mauvaise heure
Un déclencheur Schedule n8n s’exécute à la mauvaise heure parce que n8n lit son fuseau horaire à trois endroits différents. Corriger un seul de ces paramètres ne résout donc qu’une partie du problème. Ces trois paramètres sont la variable TZ du conteneur, la valeur par défaut de l’instance GENERIC_TIMEZONE et le fuseau horaire défini dans un workflow individuel. Définissez les trois une fois pour que toutes les planifications ajoutées ensuite s’exécutent à l’heure attendue.
Commençons par corriger une première idée reçue. Une instance n8n auto-hébergée récemment installée ne planifie pas les exécutions en UTC (temps universel coordonné). L’horloge du conteneur est en UTC, car l’image officielle ne définit pas de TZ. La planification est une couche distincte. La valeur par défaut documentée par n8n pour GENERIC_TIMEZONE est America/New_York (en août 2026). Une instance laissée dans sa configuration initiale exécute donc ses Schedule Triggers selon l’heure de New York. C’est pourquoi le décalage signalé correspond rarement à l’écart réel de l’utilisateur par rapport à l’UTC. Un administrateur situé à Berlin qui demande une exécution à 06:00 l’obtient à 12:00 heure locale, et à 11:00 pendant les semaines de mars où les États-Unis sont déjà passés à l’heure d’été, mais pas encore l’Europe.
Les trois couches de fuseau horaire, et celle qui prévaut
TZ est le fuseau horaire du système d’exploitation dans le conteneur. La documentation de n8n le décrit comme la variable qui définit le fuseau horaire du système afin de contrôler ce que renvoient les scripts et les commandes comme date. Elle détermine ce que date affiche dans le conteneur, l’horodatage ajouté à une ligne de journal du conteneur, la valeur renvoyée par new Date() dans un nœud Code et ce que voit tout script shell que vous exécutez dans le conteneur. Elle n’a aucun effet sur l’heure d’exécution d’un Schedule Trigger.
GENERIC_TIMEZONE est le fuseau horaire de l’instance n8n. La documentation le désigne comme le fuseau horaire de l’instance n8n et précise qu’il est important pour les nœuds de planification comme Cron. Ici, Cron désigne la syntaxe standard de planification basée sur l’heure. n8n l’expose dans l’option Custom (Cron) du Schedule Trigger.
Le fuseau horaire du workflow se définit pour chaque workflow. Ouvrez le workflow dans le canvas, sélectionnez les trois points dans l’angle supérieur droit, sélectionnez Settings, puis modifiez la valeur Timezone. Ce réglage remplace GENERIC_TIMEZONE pour ce workflow uniquement.
Pour un Schedule Trigger, l’ordre est fixe. n8n utilise le fuseau horaire du workflow s’il est défini. Sinon, il utilise le fuseau horaire de l’instance indiqué par GENERIC_TIMEZONE. À défaut, il utilise sa valeur par défaut intégrée, America/New_York. TZ n’est consulté à aucune étape de cette décision.
Pour les dates dans vos nœuds, la réponse dépend de l’horloge interrogée par le code. Luxon, la bibliothèque de gestion des dates utilisée par les expressions n8n, utilise le fuseau horaire n8n. $now et $today suivent donc le même ordre workflow puis instance que le déclencheur. Le new Date() JavaScript standard dans un nœud Code interroge le système d’exploitation. Il suit donc TZ. Cette séparation est à l’origine de la plupart des confusions : le déclencheur peut fonctionner correctement alors que chaque horodatage écrit par le workflow est décalé de plusieurs heures.
Définissez les trois dans le fichier Compose
Placez TZ et GENERIC_TIMEZONE l’un à côté de l’autre dans le fichier, afin que personne ne définisse l’un en oubliant l’autre. L’extrait ci-dessous correspond à la partie liée au fuseau horaire d’un service fonctionnel. Le reste du fichier, le reverse proxy et le certificat, provient de n8n auto-hébergé sur un VPS derrière HTTPS.
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:Appliquez la modification avec docker compose up -d, et non avec docker compose restart. Un redémarrage relance le même conteneur avec l’environnement avec lequel il a été créé. Le fichier est donc modifié, mais pas le processus en cours. up -d détecte l’environnement modifié et recrée le conteneur. Si vous conservez ces valeurs dans un fichier env plutôt que de les définir directement, la même règle de recréation s’applique. Le guide gestion du fichier env et des secrets de Compose explique où ce fichier est lu.
Utilisez un nom de zone IANA (Internet Assigned Numbers Authority) au format Region/City, comme Europe/Berlin ou America/Sao_Paulo. Ces noms intègrent les règles de changement d’heure du lieu concerné. Le décalage change donc lorsque l’heure locale change. Un nom à décalage fixe comme Etc/GMT+5 ne change jamais selon les saisons, et son signe est inversé par rapport à ce que l’on pourrait penser. Exécutez LC_ALL=C TZ=Etc/GMT+5 date +%z : la commande affiche -0500. Évitez ces noms.
Pourquoi n’en définir qu’un seul ne corrige que la moitié du problème
Définissez uniquement GENERIC_TIMEZONE : le Schedule Trigger se déclenche à l’heure souhaitée, tandis que tout ce qui lit le système d’exploitation reste en UTC. Un nœud Code qui appelle new Date().toString() renvoie une chaîne UTC, les lignes des journaux du conteneur sont horodatées en UTC et les noms de fichiers construits à partir de l’horloge système basculent à la mauvaise heure de minuit.
Définissez uniquement TZ : l’inverse se produit. docker compose exec n8n date affiche votre heure locale, ce qui donne l’impression que la configuration fonctionne, tandis que le Schedule Trigger utilise toujours America/New_York et se déclenche six heures avant ou après l’heure demandée. C’est la configuration qui fait perdre le plus de temps, car la première vérification effectuée par la plupart des utilisateurs est alors concluante.
Définissez le fuseau horaire d’un workflow, puis modifiez GENERIC_TIMEZONE plus tard : ce workflow ignore la modification. La valeur du workflow est prioritaire et le reste jusqu’à ce que quelqu’un ouvre les paramètres de ce workflow. Lorsqu’un workflow s’exécute à une heure inhabituelle alors que les workflows voisins fonctionnent normalement, c’est presque toujours la cause.
Vérifiez les horloges au lieu de faire des suppositions
Comparez directement l’hôte et le conteneur.
date
docker compose exec n8n date
docker compose exec n8n printenv TZ GENERIC_TIMEZONELes deux premières commandes doivent afficher la même heure système une fois que TZ est défini. printenv affiche une ligne pour chaque variable présente. Deux lignes indiquent que les deux variables sont définies. Une seule ligne indique que la configuration n’est que partiellement corrigée.
Demandez ensuite directement à n8n, depuis un workflow, car un shell de conteneur ne peut pas vous indiquer le fuseau horaire défini au niveau du workflow. Ajoutez un nœud Code au workflow qui pose problème, puis exécutez-le une fois avec Execute Workflow.
return [
{
json: {
n8n_time: $now.toISO(),
n8n_zone: $now.zoneName,
system_time: new Date().toString(),
},
},
];n8n_zone est le fuseau horaire que le Schedule Trigger de ce workflow utilisera, après résolution selon l’ordre workflow, puis instance. Il répond donc directement à la question. system_time contient le fuseau horaire propre au conteneur, issu de TZ. Exécutez ce code dans le workflow qui pose problème, et non dans un nouveau workflow, car le paramètre défini au niveau du workflow est enregistré avec celui-ci. Si ces deux valeurs diffèrent, vous avez trouvé le problème sans ouvrir le moindre fichier de configuration.
Expressions cron dans le nœud Schedule Trigger
Schedule Trigger propose des intervalles fixes, de quelques secondes à plusieurs mois, ainsi que l’option Custom (Cron) pour les cas non couverts. L’expression cron est interprétée dans le fuseau horaire effectif du workflow. Ainsi, 0 6 * * * signifie 06:00 dans ce fuseau, et non 06:00 UTC. Une expression à cinq champs copiée depuis crontab guru peut être collée telle quelle. n8n accepte aussi un champ facultatif pour les secondes. La documentation le place en première position : seconde, minute, heure, jour du mois, mois, jour de la semaine.
N’encodez jamais le décalage manuellement. Écrire 0 4 * * * sur une instance en UTC pour atteindre 06:00 à Berlin est correct en hiver, mais décale l’exécution d’une heure pendant tout l’été, car Berlin est à UTC+1 en hiver et à UTC+2 en été. Définissez le fuseau horaire, puis indiquez l’heure locale souhaitée.
Ce que l’heure d’été change pour une tâche planifiée à 02:30
Une heure locale affichée sur une horloge n’est pas un instant garanti. Deux fois par an, une heure disparaît et une autre se répète. Toute tâche planifiée pendant ces périodes est concernée. Vous pouvez observer ce comportement avec date sur n’importe quel système Linux, sans utiliser n8n.
LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30'date: invalid date '2027-03-28 02:30'Ce n’est pas une erreur dans la commande. Le 2027-03-28, l’horloge de Berlin passe directement de 02:00 à 03:00. L’heure locale 02:30 n’existe donc pas ce jour-là, et date refuse de la convertir en instant. Une tâche ancrée sur 02:30 heure locale n’a aucun instant auquel s’exécuter. Les heures voisines ne posent pas de problème : date -d '2027-03-28 01:30' est interprétée en CET et date -d '2027-03-28 03:30' en CEST.
La transition automnale produit l’effet inverse. Le 2027-10-31, l’horloge de Berlin recule de 03:00 à 02:00. L’heure 02:30 se produit donc deux fois.
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
1824946200Il s’agit de deux instants différents, tous deux appelés 02:30 heure locale, et séparés de 3600 secondes. Une tâche planifiée à cette heure s’exécute soit deux fois, soit une seule fois à une heure que personne n’a choisie. Aucun de ces résultats ne convient à une facturation ou à une rotation de sauvegardes. Déplacez la planification en dehors de cette période. Dans la plupart des fuseaux horaires européens et nord-américains, la plage à risque va de 00:00 à 03:00 heure locale.
Planifiez l’infrastructure en UTC et affichez l’heure locale aux utilisateurs
La réponse standard consiste à séparer les deux rôles d’un fuseau horaire. Les machines ont besoin d’un intervalle stable. Les utilisateurs ont besoin d’une heure lisible.
- Pour les tâches que personne ne surveille, définissez le fuseau horaire du workflow sur UTC. Les sauvegardes, le préchauffage du cache, l’expédition des journaux et la génération de rapports relèvent de cette catégorie. En UTC, l’intervalle entre deux exécutions correspond exactement à celui que vous avez défini, tous les jours de l’année, car l’UTC n’applique pas l’heure d’été.
- Pour les tâches consultées par une personne, conservez la planification en UTC et effectuez la conversion au moment de l’affichage. Une seule expression suffit :
{{ $now.setZone('Europe/Berlin').toFormat('yyyy-MM-dd HH:mm') }}insère l’heure locale dans le corps du message, tandis que le déclencheur reste stable.
La même séparation s’applique en dehors de n8n. Lorsqu’une partie de votre automatisation s’exécute sous la forme d’un service et d’un timer systemd sur le VPS, sa ligne OnCalendar est interprétée selon le fuseau horaire du système, qui constitue une quatrième horloge avec son propre réglage. En configurant tous les ordonnanceurs sur UTC, vous n’avez plus qu’une règle à retenir au lieu de quatre. Cela compte également pour tout ce qui récapitule une période, car un workflow d’agent IA n8n auquel vous demandez les chiffres de la veille utilisera discrètement une période de 24 heures différente selon le fuseau qui a été appliqué.
Modes de panne et sortie affichée
Tout s’exécute avec environ six heures de décalage. GENERIC_TIMEZONE n’a jamais été défini ; la valeur par défaut intégrée America/New_York s’applique donc. docker compose exec n8n printenv GENERIC_TIMEZONE n’affiche absolument rien. Définissez cette variable, puis recréez le conteneur.
Vous avez modifié le fichier Compose, mais rien n’a changé. Vous avez exécuté docker compose restart ; le conteneur a donc conservé son environnement initial. Exécutez docker compose up -d, puis vérifiez avec docker compose exec n8n printenv TZ.
Le déclencheur est correct, mais les horodatages sont incorrects. Seul GENERIC_TIMEZONE est défini. Un new Date() dans un nœud Code lit toujours le fuseau UTC du système d’exploitation. Définissez TZ avec la même valeur, puis recréez le conteneur.
Un workflow ignore le réglage de l’instance. Ce workflow contient son propre fuseau horaire dans ses paramètres, qui prend le pas sur GENERIC_TIMEZONE. Ouvrez le canvas, cliquez sur les trois points, puis sur Settings et Timezone.
Une tâche quotidienne s’est exécutée deux fois ou a ignoré un jour, une fois cette année. Son heure d’exécution se situe pendant une transition liée à l’heure d’été. Modifiez l’heure ou faites passer ce workflow en UTC.
FAQ
Pourquoi mon déclencheur de planification n8n se lance-t-il à la mauvaise heure ?
Le workflow utilise un autre fuseau horaire que celui que vous supposez. n8n utilise le fuseau horaire du workflow s’il est défini, sinon le fuseau horaire de l’instance indiqué par GENERIC_TIMEZONE, et sinon sa valeur par défaut intégrée America/New_York. Une instance auto-hébergée pour laquelle personne n’a défini GENERIC_TIMEZONE planifie les tâches selon l’heure de New York, et non selon UTC. C’est pourquoi le décalage correspond rarement à votre propre différence avec UTC. Exécutez docker compose exec n8n printenv GENERIC_TIMEZONE. Si aucune sortie ne s’affiche, cela signifie que la variable n’a jamais été définie.
Quelle est la différence entre TZ et GENERIC_TIMEZONE dans n8n ?
TZ correspond au fuseau horaire du système d’exploitation dans le conteneur. Il détermine le résultat de date dans le conteneur, les horodatages affichés dans les lignes des journaux du conteneur, le résultat de new Date() dans un nœud Code et ce que voient les scripts que vous exécutez dans le conteneur. GENERIC_TIMEZONE correspond au fuseau horaire de l’instance n8n. C’est celui qu’utilisent les nœuds de planification et les expressions Luxon telles que $now. Si vous définissez une seule de ces variables, vous pouvez obtenir un déclencheur correct avec des horodatages incorrects, ou des horodatages corrects avec un déclencheur qui se lance plusieurs heures trop tôt ou trop tard. Définissez les deux sur la même valeur.
Dois-je définir le fuseau horaire du workflow ou GENERIC_TIMEZONE ?
Définissez GENERIC_TIMEZONE comme valeur par défaut pour toute l’instance et utilisez le paramètre propre au workflow uniquement lorsqu’un workflow doit réellement utiliser un autre fuseau. La valeur du workflow est prioritaire sur celle de l’instance. Elle ne suit pas les modifications ultérieures de GENERIC_TIMEZONE. Un remplacement propre au workflow oublié peut donc être difficile à repérer plusieurs mois plus tard.
Que devient une tâche planifiée à 02:30 lors du changement d’heure ?
Cette heure locale disparaît ou se produit deux fois. LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30' renvoie date: invalid date '2027-03-28 02:30', car l’horloge de Berlin passe de 02:00 à 03:00 ce jour-là. Le 2027-10-31, la même heure affichée sur l’horloge correspond à deux instants espacés d’une heure. Évitez de planifier des tâches entre 00:00 et 03:00, heure locale, ou définissez le workflow sur UTC et convertissez l’heure en heure locale uniquement lorsqu’une personne doit la consulter.