FlowKit

n8n Luxon dates & times: working examples you can copy-paste

Published 28 July 2026 · 7 min read

Almost every n8n workflow ends up touching a date: timestamping a record, triggering a follow-up three days after an email, checking you're inside business hours before sending a notification, or displaying "July 28, 2026 at 9:00 am" in a report. Good news: n8n ships with Luxon, a modern JavaScript date library, built in — and exposes it directly in expressions through two ready-made variables: $now and $today. Mastering a dozen Luxon methods covers 95% of real-world needs — this guide walks through them with examples you can copy straight into your workflows.

$now and $today: your two starting points

In any n8n expression, $now returns a Luxon DateTime object representing the present instant, and $today the same thing with the time reset to midnight (00:00:00). These are not strings: they're rich objects on which you chain methods.

{{ $now }}                      → the present instant
{{ $today }}                    → today at 00:00:00
{{ $now.toFormat('yyyy-MM-dd') }} → "2026-07-28"

The distinction matters: to compare a record's date against "today", $today avoids false negatives caused by hours and minutes; to precisely timestamp an event, $now is the one. If you prefer working inside a Code node, the same objects are available there, along with Luxon's DateTime class — our guide to JavaScript expressions in n8n details what you can do in each.

Luxon in n8n: the cheat sheet

You want Expression to copy-paste Output
Today, ISO date {{ $now.toFormat('yyyy-MM-dd') }} 2026-08-19
French format with time {{ $now.toFormat('dd/MM/yyyy HH:mm') }} 19/08/2026 09:15
ISO 8601 for an API {{ $now.toISO() }} 2026-08-19T09:15:32.000+02:00
In 3 days {{ $now.plus(3, 'days') }} DateTime object
1 month ago {{ $now.minus(1, 'month') }} DateTime object
Tomorrow at 9:00 sharp {{ $now.plus(1, 'day').set({ hour: 9, minute: 0 }) }} DateTime object
Days between two dates {{ $json.due.diff($now, 'days').days }} e.g. 3.2
Parse 28/07/2026 {{ DateTime.fromFormat($json.date, 'dd/MM/yyyy') }} DateTime object
Convert to Paris time {{ $now.setZone('Europe/Paris') }} DateTime object
Unix timestamp (seconds) {{ $now.toSeconds() }} 1755587732

Formatting a date: toFormat and standard outputs

The .toFormat() method takes a string of tokens (documented by Luxon) describing the desired output:

{{ $now.toFormat('yyyy-MM-dd') }}        → "2026-07-28"
{{ $now.toFormat('dd/MM/yyyy HH:mm') }}  → "28/07/2026 09:15"
{{ $now.toFormat('cccc dd LLLL yyyy') }} → "Tuesday 28 July 2026"

Mind the token casing: MM is the month, mm is minutes, HH is the hour on a 24-hour clock. For localized output in another language inside an email, set a locale: {{ $now.setLocale('fr').toFormat('cccc dd LLLL yyyy') }}. And when talking to an API, prefer the standard ISO format: {{ $now.toISO() }} produces a complete, unambiguous string, timezone included.

Adding, subtracting, pinning: plus, minus and set

Date arithmetic uses .plus() and .minus(), which accept an object of durations:

{{ $now.plus({days: 3}) }}               → 3 days from now
{{ $now.minus({hours: 2, minutes: 30}) }} → 2.5 hours ago
{{ $today.plus({months: 1}) }}            → one month from now, at midnight

The .set() method rounds out the pair by pinning a specific component: {{ $today.plus({days: 1}).set({hour: 9}) }} gives "tomorrow at 9:00 am", the typical expression to feed a Wait node in "At Specified Time" mode — we cover how that works in our Wait node guide. Luxon handles edge cases correctly: adding a month to January 31 lands on February 28 (or 29), not on an invalid date.

Comparing dates and computing durations

DateTime objects compare directly with the usual operators:

{{ $json.deadline_dt < $now }}   → is the deadline past?
{{ $json.created >= $today }}    → created today?

To measure a gap, .diff() returns a Duration object from which you extract the unit you want:

{{ $now.diff(DateTime.fromISO($json.created_at), 'days').days }}

This expression returns a record's age in days (with decimals — round with Math.floor() if needed). It's exactly the calculation at the heart of any follow-up logic: "has this file gone more than 3 days without a reply?". An IF node with the condition {{ $now.diff(DateTime.fromISO($json.last_contact), 'days').days >= 3 }} routes the files due for a nudge, a pattern we apply end to end in our article on following up on incomplete submissions.

Parsing a string: fromISO and fromFormat

Dates rarely arrive as DateTime objects: an API returns an ISO string, a CSV file a local-format date, a form free text. Two methods cover the essentials:

{{ DateTime.fromISO('2026-07-28T09:00:00') }}
{{ DateTime.fromFormat('28/07/2026', 'dd/MM/yyyy') }}

fromISO handles ISO 8601 strings (what most APIs return); fromFormat takes as its second argument the exact description of the input format, using the same tokens as toFormat. One crucial point: a string that doesn't match the expected format does not throw an error — it produces an invalid DateTime that propagates silently. Check .isValid right after parsing whenever the data comes from outside.

Timezones: the topic that derails workflows

This is the number one source of time-related bugs in n8n. Three configuration layers come into play:

  • The server's clock: most Docker instances run in UTC. $now reflects the timezone configured on the n8n side, not your browser's.
  • The GENERIC_TIMEZONE environment variable: set it (for example Europe/Paris) to define the instance-wide default timezone — it's what time-sensitive nodes like the Schedule Trigger read.
  • The workflow's timezone: in each workflow's settings, a specific timezone can override the instance default.

For a one-off conversion inside an expression, .setZone() does the job: {{ $now.setZone('Europe/Paris').toFormat('HH:mm') }} displays Paris time whatever timezone the server runs in. Always use IANA identifiers (Europe/Paris, America/New_York), never fixed offsets like "UTC+1": only IANA identifiers automatically follow daylight saving transitions.

Those transitions are no folkloric detail, either: a 2009 study by Barnes and Wagner in the Journal of Applied Psychology ("Changing to daylight saving time cuts into sleep and increases workplace injuries" — see on Google Scholar) shows that the switch to daylight saving time alone measurably cuts into workers' sleep and increases workplace injuries. If a single hour's shift produces measurable physical effects on humans, imagine what it does to a workflow that sends its follow-ups "at 9 am": civil-time rules (DST, timezones) are very real traps, best modeled explicitly rather than discovered in production. For scheduled triggers specifically, our guide to the Schedule Trigger and timezones digs into exactly this.

Real cases: 3-day follow-up, business hours, emails

Follow-up at day 3. After a first email, store {{ $now.toISO() }} in your database, then a daily scheduled workflow filters with {{ $now.diff(DateTime.fromISO($json.sent_at), 'days').days >= 3 }}. Database-free alternative: a Wait node set to {{ $now.plus({days: 3}).set({hour: 9, minute: 0}) }}.

Business-hours window. Before sending a notification, an IF node checks the hour and the day: {{ $now.setZone('Europe/Paris').hour >= 9 && $now.setZone('Europe/Paris').hour < 18 && $now.weekday <= 5 }} (in Luxon, weekday runs from 1 for Monday to 7 for Sunday). Outside the window, route to a Wait that holds until the next working slot.

Readable dates in emails and reports. A raw "2026-07-28T07:15:00.000Z" in an email scares people off: {{ DateTime.fromISO($json.date).setZone('Europe/Paris').toFormat("cccc d LLLL yyyy 'at' h:mm a") }} produces "Tuesday 28 July 2026 at 9:15 AM". The same logic applies to invitations: our Google Calendar with n8n guide shows how to build event start and end dates with these expressions.

The Date & Time node: the no-code alternative

Everything above can also be done visually with the Date & Time node, which offers ready-made operations: format a date, add or subtract a duration, round, extract a component (day, month, hour), get the current date. For a team where not everyone reads expressions, it's a perfectly valid readability choice — the workflow documents itself. Luxon expressions win back the advantage as soon as several operations need chaining in a single field, or inside a Code node for more elaborate logic (if you prefer Python, note that Luxon isn't available there: see our Python Code node guide for the equivalents).

Common pitfalls

  • Confusing the server's clock with local time. $now follows the instance's timezone (often UTC on Docker), not your browser's. Set GENERIC_TIMEZONE and check the workflow's timezone before hunting for a bug elsewhere.
  • Using a fixed offset instead of an IANA identifier. "UTC+1" is wrong half the year in France; Europe/Paris tracks daylight saving transitions on its own.
  • Ignoring invalid DateTimes. DateTime.fromFormat() on a malformed string doesn't crash: it returns an invalid object that yields empty fields downstream. Check .isValid on any external data.
  • Getting the token casing wrong. MM = month, mm = minutes, HH = 24-hour clock: a dd/mm/yyyy prints minutes where the month should be — a quiet, frequent mistake.
  • Comparing a string with a DateTime. $json.date straight out of an API is a string: parse it with DateTime.fromISO() before any comparison or .diff(), or the result is meaningless.
  • Forgetting that $today has no time component. Handy for comparing days, treacherous if you use it for timestamping: everything ends up dated midnight.

Going further

These date calculations are the invisible backbone of most serious automations: without them, no follow-up at the right moment and no correctly dated report. It's exactly what powers the daily digest in the AI Inbox Pack (€79): business-hours windows to summarize only the past day's emails, delivery pinned to the user's timezone, readable dates in the summary. To complete the picture on the triggering side, the Schedule Trigger guide covers cron scheduling, and the Wait node guide covers dynamically computed pauses — Luxon's two natural companions in any time-driven workflow.

FAQ

Frequently asked questions

What is the difference between $now and $today in n8n?

Both are ready-to-use Luxon DateTime objects in expressions. $now is the exact instant the node runs (full date and time), while $today is the same date with the time reset to 00:00:00. To compare a record's date against 'today' without hours and minutes getting in the way, $today is the right pick; to timestamp an email or compute a precise delay, use $now.

Why do my n8n dates show up several hours off?

In most cases your instance runs in UTC (the default for Docker images) while you think in local time. Set the GENERIC_TIMEZONE environment variable (for example Europe/Paris) to define the instance-wide default timezone, or set the timezone in the affected workflow's settings. As a last resort, convert on the spot with .setZone('Europe/Paris') inside the expression itself.

How do I parse a date received in a custom format like 28/07/2026?

DateTime.fromISO only works for ISO 8601 strings (2026-07-28T09:00:00). For a custom format, use DateTime.fromFormat('28/07/2026', 'dd/MM/yyyy'), which explicitly describes the string's structure. Then check the result's validity (the isValid property) before moving on: a malformed string produces an invalid DateTime that propagates empty values through the whole workflow.

Should I use Luxon expressions or the Date & Time node?

Both reach the same result; the difference is readability and who's on your team. The Date & Time node covers the common operations without code (formatting, adding or subtracting a duration, rounding, extracting a component) and keeps the workflow readable for non-developers. Luxon expressions are more compact and more powerful as soon as you need to chain several operations (parse, convert timezone, compare) in a single field.

Bundle FlowKit Complet

€269