Self-Hosting n8n with Docker Compose: Complete Guide for German Servers
How to install n8n with Docker Compose: Postgres instead of SQLite, .env, volumes and updates step by step.
Luxon in n8n: produce German date formats with toFormat and setLocale and avoid typical time zone pitfalls in expressions.
n8n processes date and time internally via the JavaScript library Luxon and not via the native JavaScript Date object. You usually only notice this once a workflow outputs a date that is off by one hour, a month name appears in English instead of German, or a document suddenly lands on the wrong day at the end of the month. Once you understand how Luxon cleanly separates time zone, language, and format, you can specifically avoid exactly these errors, and you keep control over which date actually ends up on the invoice, in the CRM, or in the table.
This article deliberately focuses on formatting date and time in expressions and in the Date & Time Node, not on the time zone logic of the Schedule Trigger or of Cron expressions. That is a separate topic with its own pitfalls that deserves a separate look.
Plain JavaScript has no built-in concept for time zones or language formats that go beyond the most basic level. Luxon fills exactly this gap: every date that n8n generates internally, for example via $now or $today, is a Luxon DateTime object with its own time zone, its own language (locale), and its own methods for calculating, comparing, and formatting. According to the official documentation on working with Date and Time in n8n date values are passed between nodes as strings and must first be parsed again with Luxon functions for calculations or formatting. The native JavaScript Date() does not respect the time zone set in n8n, which is why the documentation explicitly recommends sticking with Luxon for everything related to date and time.
For the actual formatting, several approaches are available in expressions, which are differently well suited to German requirements:
.toFormat(fmt): the native Luxon method with which you build a format yourself using fixed tokens, for example dd.MM.yyyy. It works everywhere, including in the Code Node..toLocaleString(opts): returns a localized, meaning language-dependent, format. Without a set locale, the result follows the default language of the n8n instance, which in practice often means English.format(): an n8n-specific extension of the expression language for date formatting, which according to the DateTime Expression Reference is not available in the Code Node. There you instead fall back on the native .toFormat().MM/DD/YYYY or YYYY-MM-DD, plus an option for a custom format. According to the documentation on the Date & Time Node n8n supports all formats for this that Luxon itself knows, whereby the tokens are case-sensitive.For a purely numeric German date, .toFormat() with a fixed token pattern is usually enough, while .toLocaleString() and .setLocale() become important as soon as spelled-out month or weekday names are supposed to appear in German.
For the classic German notation TT.MM.JJJJ, a fixed expression without locale dependency is enough:
{{$now.toFormat('dd.MM.yyyy')}} for example produces 20.07.2026.{{$now.toFormat('dd.MM.yyyy HH:mm')}} adds the time in 24-hour format.As soon as spelled-out names come into play, for example for an invoice or a mail merge, you additionally need the German locale, because otherwise the month name is output in English:
{{$now.setLocale('de-DE').toLocaleString({month: 'long', day: 'numeric', year: 'numeric'})}} produces a result like 20. Juli 2026.{{$now.setLocale('de-DE').monthLong}} directly returns the spelled-out month name, for example Juli.Important here: .setLocale() only changes the language of the output, not the time zone. You must set both settings separately, otherwise you get a German word for the month, but under certain circumstances still the wrong time.
Most errors do not occur in the format itself, but in the time zone in which formatting takes place. A few pitfalls that keep coming up in practice:
America/New York, while on n8n Cloud the system falls back to GMT. $now and $today follow this instance time zone, unless a workflow-specific setting or a GENERIC_TIMEZONE environment variable specifies otherwise. Anyone who overlooks this gets timestamps that deviate by several hours from German time..setZone(): .toFormat() and .toLocaleString() always output the time in the time zone that the DateTime object currently has, not automatically Europe/Berlin. If a date comes from an API in UTC or in a foreign time zone, you should first convert it with .setZone('Europe/Berlin') and only format it afterward, otherwise a document created at 23:30 German time suddenly ends up on the following day in the report..plus({days: n}) across such a change and then format the time, the time portion can shift by one hour, even though the calendar day is correct. For pure date-without-time comparisons this is usually uncritical, but for time-critical processes you should plan for it..toISO() with offset: Luxon's ISO string contains the time zone offset by default, for example +02:00. Systems that instead expect pure UTC or an offset-less string otherwise interpret this value incorrectly. Check in the target system which format is actually expected before blindly passing on the result.A typical case from practice: a workflow retrieves a document date from an API that delivers UTC timestamps, and is supposed to generate a German invoice date from it. The expression for this looks combined like this:
{{ $json.createdAt.toDateTime().setZone('Europe/Berlin').setLocale('de-DE').toFormat('dd.MM.yyyy') }}The order here is no coincidence: first the native JavaScript timestamp is converted into a Luxon DateTime, then shifted into the correct time zone, then the language is set, and only at the end is it formatted. If you swap the time zone and formatting steps, you get a correctly looking German date, but possibly for the wrong calendar day.
.toFormat() and .toLocaleString()?.toFormat() follows a fixed token pattern that you define yourself, such as dd.MM.yyyy, and always delivers the same layout regardless of the locale. .toLocaleString(), on the other hand, follows the set language and automatically adjusts the order as well as the spelling, which makes the difference especially with spelled-out month or weekday names.
Without an explicit .setLocale('de-DE'), Luxon uses the default language of the n8n instance, which is often set to English. The time zone of the instance and its language are two separate settings, which is why a correctly set Europe/Berlin alone does not yet guarantee a German month name.
Whenever a date comes from an external source such as an API, a database, or a form, yes. Such values are often in UTC or in a foreign time zone, and only $now or $today automatically follow the instance or workflow time zone configured in n8n.
No, separate rules apply there for the execution time itself, not for the formatting of date values in expressions. This article deals exclusively with how an already existing date within a workflow is formatted correctly.
According to the documentation on the Date & Time Node, n8n supports all the tokens that Luxon itself knows, whereby uppercase and lowercase each have their own meaning. For a complete overview of all available tokens, it is worth taking a look at the linked Luxon formatting reference directly from the n8n documentation.
Founder of NordFlux. Spent four years automating processes at enterprise scale at Dräger, and now brings that depth to the mid-market — pragmatic and with full data sovereignty.
Certifications
How to install n8n with Docker Compose: Postgres instead of SQLite, .env, volumes and updates step by step.
By default, n8n runs in America/New_York instead of Europe/Berlin. Here is how to correctly set GENERIC_TIMEZONE and the workflow timezone.
Incorrectly formatted invoice dates or shifted timestamps caused by time zone pitfalls can cost you trust with customers and authorities when it matters most. NordFlux develops and reviews your n8n workflows so date and time logic stays correct even with international data sources. In an initial conversation we look at your critical expressions together.