DEV Community

Pirate Prentice
Pirate Prentice

Posted on

n8n DateTime Node: Format Dates, Calculate Differences, and Convert Timezones [Free Workflow JSON]

Working with dates in n8n used to mean writing custom JavaScript in a Code node just to reformat a timestamp or calculate "how many days ago." The DateTime node (introduced in n8n 0.221) handles all of that natively — no code required.

This guide covers every operation the DateTime node supports, the formatting tokens you'll actually use, and three real-world workflow patterns with free JSON you can import today.


What the n8n DateTime Node Does

The DateTime node performs four categories of operations:

  1. Format a date — convert any date/time value into a human-readable or machine-readable string
  2. Calculate a difference — find the gap between two dates in seconds, minutes, hours, days, weeks, months, or years
  3. Add or subtract time — offset a date by a duration
  4. Extract a part — pull out just the year, month, day, hour, etc.

All operations use Luxon under the hood, which is also available in n8n expressions as $now, $today, and DateTime.


Operation 1: Format a Date

Use case: You receive an ISO 8601 timestamp from an API (2026-07-02T10:25:00.000Z) and need to display it as July 2, 2026 or 02/07/2026 in an email or Slack message.

Settings:

  • Operation: Format a Date
  • Date: The input field containing the timestamp (e.g., {{ $json.created_at }})
  • Format: Choose a preset or enter a custom token string
  • Output Field Name: The field name to write the result into

Common format tokens:

Token Output example Meaning
yyyy-MM-dd 2026-07-02 ISO date
dd/MM/yyyy 02/07/2026 EU date
MM/dd/yyyy 07/02/2026 US date
MMMM d, yyyy July 2, 2026 Long date
EEE, MMM d Thu, Jul 2 Short weekday
HH:mm:ss 10:25:00 24h time
hh:mm a 10:25 AM 12h time
yyyy-MM-dd'T'HH:mm:ssZZ 2026-07-02T10:25:00+00:00 ISO 8601 with offset
X 1751453100 Unix timestamp (seconds)
x 1751453100000 Unix timestamp (milliseconds)

Timezone input: If your incoming date has no timezone info, set Input Timezone to the correct zone (e.g., America/Chicago) to avoid silent UTC mismatches.


Operation 2: Calculate a Difference

Use case: A support ticket was opened on 2026-06-25. Today is 2026-07-02. How many days has it been open?

Settings:

  • Operation: Calculate a Date Difference
  • Start Date: Earlier date (e.g., {{ $json.opened_at }})
  • End Date: Later date (e.g., {{ $now }})
  • Units: days (or seconds, minutes, hours, weeks, months, years)
  • Output Field Name: e.g., days_open

The result is an integer. For the example above: 7.

Tip: Use seconds for SLA calculations, then convert to minutes/hours in downstream expressions — integers are easier to compare than decimal days.


Operation 3: Add or Subtract Time

Use case: You want a "follow-up due" date that is 3 business days after a lead signs up, or an expiry timestamp 30 days from now.

Settings:

  • Operation: Add to a Date
  • Date: Base date
  • Duration: The amount to add (can be negative to subtract)
  • Duration Unit: seconds | minutes | hours | days | weeks | months | years
  • Output Field Name: e.g., follow_up_date

Example — 30-day trial expiry:

Base date: {{ $now }}
Duration: 30
Unit: days
Output: trial_expires_at
Enter fullscreen mode Exit fullscreen mode

Then format trial_expires_at in a second DateTime node (or in an expression) to get a human-readable string for the welcome email.

Chaining two DateTime nodes is the right pattern: one to do arithmetic, one to format the result.


Operation 4: Extract a Part

Use case: You only need the hour from a timestamp to decide which Slack channel to route an alert to (business hours vs. on-call).

Settings:

  • Operation: Extract Part of a Date
  • Date: Input timestamp
  • Part: year | month | day | hour | minute | second | millisecond | weekday | weekNumber | quarter
  • Output Field Name: e.g., alert_hour

Weekday note: Returns 1 (Monday) through 7 (Sunday) per ISO 8601. If you need 0-indexed Sunday-first, use an expression: {{ DateTime.fromISO($json.created_at).weekday % 7 }}.


Using Dates in Expressions (Without the Node)

The DateTime node is great for storing results in a field, but sometimes you just need a date inline. n8n's expression engine exposes Luxon directly:

// Current UTC time as ISO string
{{ $now.toISO() }}

// Today's date in US format
{{ $today.toFormat('MM/dd/yyyy') }}

// 7 days from now
{{ $now.plus({ days: 7 }).toISO() }}

// Start of current month
{{ $now.startOf('month').toISODate() }}

// Days since a field date
{{ $now.diff(DateTime.fromISO($json.created_at), 'days').days | round }}

// Is a date in the past?
{{ DateTime.fromISO($json.expires_at) < $now }}
Enter fullscreen mode Exit fullscreen mode

These work in any n8n expression field — IF node conditions, Set node values, HTTP Request body templates, etc.


Common Gotchas

1. Timezone drift
If an incoming timestamp has no Z or offset suffix, n8n treats it as UTC. Pass America/Chicago (or whatever zone it actually represents) in the Input Timezone field to correct it.

2. String vs. Date type
The DateTime node accepts strings, numbers (Unix ms/s), and JavaScript Date objects. If your upstream node outputs a number like 1751453100000, n8n auto-detects it as Unix milliseconds. If it outputs a string without a recognizable format, use the Input Format field to tell Luxon how to parse it (e.g., MM/dd/yyyy HH:mm).

3. Two nodes for "format the result of arithmetic"
Add/subtract outputs a date object. If you want a human-readable result, chain a second DateTime node set to Format a Date — trying to format in the same node as arithmetic isn't supported.

4. $today vs $now
$today is midnight UTC at the start of today. $now is the current instant. Use $today for date-only comparisons; $now for timestamps.

5. Locale in format strings
Token MMMM outputs month names in English. If you need localized month names, use DateTime.fromISO($json.date).setLocale('de').toFormat('MMMM') in an expression.


Workflow Pattern 1: Overdue Ticket Alert

Every hour, check a Google Sheet of support tickets. For any ticket open > 48 hours with no reply, send a Slack alert.

Schedule Trigger (every hour)
→ Google Sheets (Get all rows)
→ Filter (status = "open")
→ DateTime [Difference] (opened_at → now, unit: hours → field: hours_open)
→ Filter (hours_open > 48)
→ Slack (Send message: "Ticket #{{ $json.ticket_id }} open {{ $json.hours_open }}h — no reply")
Enter fullscreen mode Exit fullscreen mode

Free workflow JSON:

{
  "name": "Overdue Ticket Alert",
  "nodes": [
    {
      "parameters": { "rule": { "interval": [{ "field": "hours", "hoursInterval": 1 }] } },
      "name": "Every Hour",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [0, 0]
    },
    {
      "parameters": {
        "operation": "dateDiff",
        "date": "={{ $json.opened_at }}",
        "date2": "={{ $now.toISO() }}",
        "outputFieldName": "hours_open",
        "options": { "unit": "hours" }
      },
      "name": "Hours Open",
      "type": "n8n-nodes-base.dateTime",
      "typeVersion": 2,
      "position": [240, 0]
    },
    {
      "parameters": {
        "conditions": {
          "number": [{ "value1": "={{ $json.hours_open }}", "operation": "larger", "value2": 48 }]
        }
      },
      "name": "Over 48h?",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 1,
      "position": [480, 0]
    },
    {
      "parameters": {
        "text": "=Ticket #{{ $json.ticket_id }} open {{ $json.hours_open }}h — no reply yet.",
        "channelId": "support-alerts"
      },
      "name": "Slack Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [720, 0]
    }
  ],
  "connections": {
    "Every Hour": { "main": [[{ "node": "Hours Open", "type": "main", "index": 0 }]] },
    "Hours Open": { "main": [[{ "node": "Over 48h?", "type": "main", "index": 0 }]] },
    "Over 48h?": { "main": [[{ "node": "Slack Alert", "type": "main", "index": 0 }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow Pattern 2: Trial Expiry Reminder Email

When a user signs up, calculate their trial expiry date (30 days out) and store it. A daily job then emails anyone whose trial expires tomorrow.

Webhook (new signup)
→ DateTime [Add] (now + 30 days → trial_expires_at)
→ DateTime [Format] (trial_expires_at → "MMMM d, yyyy" → trial_expires_display)
→ Airtable (Create record with trial_expires_at + trial_expires_display)

Schedule Trigger (daily 08:00)
→ Airtable (Filter: trial_expires_at = tomorrow)
→ Gmail (Send: "Your trial ends on {{ $json.trial_expires_display }}")
Enter fullscreen mode Exit fullscreen mode

Workflow Pattern 3: Business-Hours Router

Route incoming webhook events to the right Slack channel based on whether they arrive during business hours (Mon–Fri, 09:00–17:00 CT).

Webhook
→ DateTime [Extract Part] (now → hour → current_hour, timezone: America/Chicago)
→ DateTime [Extract Part] (now → weekday → current_weekday)
→ IF (current_weekday <= 5 AND current_hour >= 9 AND current_hour < 17)
  → True: Slack #support (business hours)
  → False: Slack #oncall (after hours)
Enter fullscreen mode Exit fullscreen mode

The Full n8n Workflow Starter Pack

These patterns are part of the n8n Workflow Starter Pack — 20+ pre-built, documented workflow JSONs covering the most common automation patterns: webhooks, scheduling, database ops, API integrations, error handling, and date/time logic.

One-time purchase, import-ready JSON, no recurring fees.

Get the n8n Workflow Starter Pack ($29)


Quick Reference

Task Operation Key field
Reformat a timestamp Format a Date Format tokens (e.g., MMMM d, yyyy)
Days/hours between two dates Calculate Difference Units dropdown
Add 30 days to now Add to a Date Duration + Unit
Get the hour from a timestamp Extract Part Part = hour
Current time in expression $now.toISO()
7 days from now in expression $now.plus({days:7}).toISO()
Parse custom format Format a Date Input Format field

What's Next?

Drop a comment with your date/time use case — I read every one. 👇

Top comments (1)

Collapse
 
pirateprentice profile image
Pirate Prentice

What datetime patterns are you solving in your workflows? For me the most common use case is business-hours routing — checking whether it's within 9-5 Mon-Fri before deciding whether to alert on-call or queue for morning. Drop your use case below 👇