n8n Date & Time Node: Format, Calculate & Schedule Dates (2025)
Almost every useful automation eventually touches a date. You want to send a reminder three days from now, stamp an email with today's date, calculate how many days are left until a subscription renews, or fire a workflow at 9 AM in the customer's timezone. In n8n, the Date & Time node, together with a handful of Luxon expressions, handles all of it. This tutorial walks through formatting, timezone conversion, date math, difference calculations, and the pitfalls that silently break workflows.
How n8n represents dates (and why it matters)
Under the hood, n8n uses Luxon, a modern JavaScript date library, for all date handling. This is the single most important thing to understand: a date in n8n can exist in two very different forms.
- A string — e.g.
"2025-03-14T10:30:00Z". This is just text. You cannot do math on it directly. - A Luxon DateTime object — a real date object you can format, add to, subtract from, and compare.
Most bugs come from confusing the two. If you try to add "3 days" to a plain string, you get nonsense. The Date & Time node (and the expressions below) exist to convert strings into DateTime objects, operate on them, then convert back to a formatted string for output.
The two built-in date helpers: {{ $now }} and {{ $today }}
Before touching the node, know these two expressions. They work anywhere you can write an expression.
{{ $now }}— the current date and time as a Luxon DateTime, in your instance timezone. Use it for timestamps.{{ $today }}— the current date at midnight (00:00), time stripped. Use it when you only care about the calendar day, such as "days until renewal".
Because both return DateTime objects, you can chain Luxon methods straight onto them:
{{ $now.toFormat('yyyy-MM-dd') }}→2025-03-14{{ $now.plus({ days: 3 }).toISO() }}→ an ISO timestamp three days ahead{{ $today.weekdayLong }}→Friday
Formatting dates with Luxon tokens
Formatting turns a DateTime into a human-readable string. The method is .toFormat() and it takes a token string. The most common tokens:
yyyy— 4-digit year (2025) ·yy— 2-digit (25)MM— month number (03) ·MMMM— full name (March) ·MMM— short (Mar)dd— day of month (14) ·d— no leading zero (14)HH— 24-hour (14) ·hh— 12-hour (02) ·a— AM/PMmm— minutes ·ss— secondsEEEE— weekday name (Friday) ·ccc— short weekday (Fri)
Format a timestamp for an email: a friendly header like "Friday, March 14, 2025 at 2:30 PM" comes from:
{{ $now.toFormat("EEEE, MMMM d, yyyy 'at' h:mm a") }}
Note the literal word at is wrapped in single quotes so Luxon does not interpret the letters as tokens. For machine-to-machine use, prefer ISO: {{ $now.toISO() }}, which every API and database understands.
Using the Date & Time node
The expressions above are perfect inline, but the Date & Time node gives you a no-code UI for the same operations — ideal when you want the workflow to be readable by teammates. Its main actions are:
- Get Current Date — outputs "now" as a field, with an optional timezone and output format.
- Format a Date — takes an input date (often a string from an earlier node) and reformats it. This is how you safely turn a string into a formatted date.
- Add / Subtract Time — add or remove years, months, days, hours, minutes.
- Get Time Between Dates — the difference between two dates in a unit you choose (days, hours, etc.).
- Extract Part of a Date — pull out just the year, month, day, or weekday.
A typical pattern: an incoming webhook gives you order.created_at as a string. Drop a Date & Time node set to Format a Date, point it at that field, and now downstream nodes receive a proper date you can trust.
See scheduling in action: Our AI-Curated Weekly Newsletter template is a working example of date handling done right — it runs on a cron Schedule Trigger, pulls the week's RSF feeds, and produces a date-stamped digest header (e.g. "Week of March 10–14") using exactly the Luxon formatting shown here. It is the fastest way to see $now, formatting, and scheduling combined in a real workflow. Get the AI Weekly Newsletter template on Gumroad →
Adding and subtracting time: "3 days from now" for reminders
Reminders are the classic use case. To schedule a follow-up email three days out, compute the target date:
{{ $now.plus({ days: 3 }).toISO() }}
The .plus() and .minus() methods accept any combination: { days: 3, hours: 2 }, { weeks: 1 }, { months: 1 }. To subtract, use .minus({ days: 7 }) for "one week ago".
Follow-up 2 business days later
Business days need to skip weekends. There is no single Luxon token for this, so use a small expression that checks the weekday (weekday returns 1=Monday through 7=Sunday):
- If today is Thursday (4), +2 business days should land on Monday, not Saturday.
- A reliable approach: add days one at a time in a Code node, skipping when
weekday > 5. In an expression, a quick approximation for a Thursday/Friday start is to add 4 days instead of 2, but a Code node loop is the robust answer for production.
For most reminder workflows, a Code node with a short loop that increments until it has counted two weekdays is worth the five extra minutes — it prevents follow-ups landing in a weekend inbox.
Calculating differences: days-until-renewal
To compute how many days remain until a subscription renews, subtract the two dates and read the result in days. Given a renewal date field renewal_date:
{{ DateTime.fromISO($json.renewal_date).diff($today, 'days').days }}
Here DateTime.fromISO() converts the stored string into a DateTime, .diff() computes the gap against $today, and .days reads it as a number. Wrap it in Math.floor(...) if you want a clean integer. You can then branch with an IF node: if days-until-renewal equals 7, send a "your plan renews next week" email.
Timezones: the source of most date bugs
This is where automations quietly break. n8n stores and computes dates in UTC by default. A meeting at "9 AM" means nothing without a timezone — 9 AM UTC is 4 AM in New York.
To send a timezone-correct meeting reminder, convert explicitly with .setZone():
{{ DateTime.fromISO($json.meeting_time).setZone('America/New_York').toFormat('h:mm a') }}
Use IANA zone names (America/New_York, Europe/London, Asia/Tokyo) — never abbreviations like EST, which are ambiguous around daylight saving. A few rules that prevent 90% of timezone pain:
- Store dates in ISO/UTC. Convert to local time only at the last step, for display.
- Set your instance timezone in Settings so
$nowand the Schedule Trigger agree with your expectations. - When a Schedule Trigger "fires at the wrong time", check the workflow's timezone setting first — it overrides the instance default.
Extracting parts of a date
Sometimes you only need one component — to route by weekday, group by month, or label a folder by year. Luxon exposes each part as a property:
{{ $now.day }}→ 14 ·{{ $now.month }}→ 3 ·{{ $now.year }}→ 2025{{ $now.weekday }}→ 5 (numeric) ·{{ $now.weekdayLong }}→ Friday{{ $now.hour }}·{{ $now.minute }}
Example: only run a step on weekdays by putting {{ $now.weekday <= 5 }} in an IF node.
Common pitfalls checklist
- String vs DateTime: always
DateTime.fromISO()a stored string before doing math on it. - UTC confusion: compute in UTC, convert to local only for display.
- Token case:
MMis month,mmis minutes;DD/dddiffer too. Case matters. - Literal text in formats: wrap words like "at" in single quotes inside
toFormat(). - Invalid dates: if
fromISO()gets a bad string it returns "Invalid DateTime" instead of erroring — validate inputs.
Master these five operations — format, add/subtract, diff, timezone-convert, extract — and you can automate any date-driven workflow: renewal reminders, scheduled digests, SLA timers, and timezone-correct notifications.
Ready to automate? Get this template on Gumroad →