Introduction

Working with dates, times, and timestamps is essential for tasks like naming backups, rotating logs, scheduling operations, and adding deployment metadata. Ansible provides the ansible_date_time fact and powerful Jinja2 filters for all date/time operations.

This guide covers everything from basic timestamp usage to advanced date arithmetic and conditional time-based logic.

The ansible_date_time Fact

The ansible_date_time variable is automatically populated when gather_facts: true (the default). It contains:

{
  "ansible_date_time": {
    "date": "2026-04-20",
    "day": "20",
    "epoch": "1745187600",
    "epoch_int": "1745187600",
    "hour": "22",
    "iso8601": "2026-04-20T22:00:00Z",
    "iso8601_basic": "20260420T220000000000",
    "iso8601_basic_short": "20260420T220000",
    "iso8601_micro": "2026-04-20T22:00:00.000000Z",
    "minute": "00",
    "month": "04",
    "second": "00",
    "time": "22:00:00",
    "tz": "UTC",
    "tz_dst": "UTC",
    "tz_offset": "+0000",
    "weekday": "Monday",
    "weekday_number": "1",
    "weeknumber": "16",
    "year": "2026"
  }
}

Important: ansible_date_time is captured at the start of fact gathering. It does NOT update during playbook execution. For real-time timestamps within a playbook, use the now() function or strftime filter.

Basic Usage

Display Date and Time

---
- name: Date and time examples
  hosts: all
  gather_facts: true
  tasks:
    - name: Show full date_time object
      ansible.builtin.debug:
        var: ansible_date_time

    - name: Show ISO 8601 timestamp
      ansible.builtin.debug:
        msg: "Current time: {{ ansible_date_time.iso8601 }}"

    - name: Show date only
      ansible.builtin.debug:
        msg: "Today is {{ ansible_date_time.date }} ({{ ansible_date_time.weekday }})"

Use in File Names

---
- name: Timestamp in filenames
  hosts: all
  gather_facts: true
  tasks:
    - name: Create backup with date stamp
      ansible.builtin.copy:
        src: /etc/nginx/nginx.conf
        dest: "/backup/nginx.conf.{{ ansible_date_time.date }}"
        remote_src: true

    - name: Create backup with full timestamp (no colons)
      ansible.builtin.copy:
        src: /etc/nginx/nginx.conf
        dest: "/backup/nginx.conf.{{ ansible_date_time.iso8601_basic_short }}"
        remote_src: true
      # Result: nginx.conf.20260420T220000

Use in Variables

---
- name: Date-based variables
  hosts: all
  gather_facts: true
  vars:
    backup_dir: "/backup/{{ ansible_date_time.date }}"
    log_file: "/var/log/deploy-{{ ansible_date_time.epoch }}.log"
    release_tag: "v1.0.0-{{ ansible_date_time.iso8601_basic_short }}"
  tasks:
    - name: Create daily backup directory
      ansible.builtin.file:
        path: "{{ backup_dir }}"
        state: directory
        mode: '0755'

The now() Function and strftime Filter

For real-time timestamps (not captured at fact-gather time), use now():

---
- name: Real-time timestamps
  hosts: all
  gather_facts: false  # Don't need facts for now()
  tasks:
    - name: Current UTC time
      ansible.builtin.debug:
        msg: "Now: {{ now(utc=true) }}"

    - name: Custom format with strftime
      ansible.builtin.debug:
        msg: "{{ now(utc=true).strftime('%Y-%m-%d_%H-%M-%S') }}"
      # Result: 2026-04-20_22-15-30

    - name: Unix epoch
      ansible.builtin.debug:
        msg: "Epoch: {{ now(utc=true).strftime('%s') }}"

Common strftime Formats

FormatExampleDescription
%Y-%m-%d2026-04-20ISO date
%H:%M:%S22:15:30Time (24h)
%Y%m%d%H%M%S20260420221530Compact timestamp
%s1745187600Unix epoch
%AMondayWeekday name
%BAprilMonth name
%d/%m/%Y20/04/2026European date
%m/%d/%Y04/20/2026US date
%Y-%m-%dT%H:%M:%SZ2026-04-20T22:15:30ZISO 8601

Advanced Patterns

Date Arithmetic (to_datetime Filter)

---
- name: Date arithmetic
  hosts: localhost
  gather_facts: true
  tasks:
    - name: Calculate days since epoch
      ansible.builtin.debug:
        msg: "Days since epoch: {{ (ansible_date_time.epoch | int) // 86400 }}"

    - name: Check if file is older than 7 days
      ansible.builtin.find:
        paths: /tmp
        age: 7d
        recurse: false
      register: old_files

    - name: Show old files
      ansible.builtin.debug:
        msg: "{{ old_files.matched }} files older than 7 days"

Conditional Logic Based on Time

---
- name: Time-based conditionals
  hosts: all
  gather_facts: true
  tasks:
    - name: Only run during business hours (9-17 UTC)
      ansible.builtin.debug:
        msg: "Running during business hours"
      when: ansible_date_time.hour | int >= 9 and ansible_date_time.hour | int < 17

    - name: Only run on weekdays
      ansible.builtin.debug:
        msg: "Weekday deployment"
      when: ansible_date_time.weekday_number | int >= 1 and ansible_date_time.weekday_number | int <= 5

    - name: Only run on first day of month
      ansible.builtin.debug:
        msg: "Monthly maintenance"
      when: ansible_date_time.day == "01"

    - name: Weekend maintenance window
      ansible.builtin.debug:
        msg: "Weekend maintenance"
      when: ansible_date_time.weekday in ['Saturday', 'Sunday']

Deployment Metadata

---
- name: Deployment with timestamp metadata
  hosts: webservers
  gather_facts: true
  vars:
    deploy_version: "2.1.0"
  tasks:
    - name: Write deployment manifest
      ansible.builtin.copy:
        dest: /opt/app/DEPLOY_INFO
        content: |
          version: {{ deploy_version }}
          deployed_at: {{ ansible_date_time.iso8601 }}
          deployed_by: {{ lookup('env', 'USER') }}
          target_host: {{ inventory_hostname }}
          ansible_version: {{ ansible_version.full }}
        mode: '0644'

    - name: Tag deployment in log
      ansible.builtin.lineinfile:
        path: /var/log/deployments.log
        line: "{{ ansible_date_time.iso8601 }} | v{{ deploy_version }} | {{ inventory_hostname }}"
        create: true
        mode: '0644'

Log Rotation with Date Stamps

---
- name: Rotate logs with timestamps
  hosts: all
  gather_facts: true
  tasks:
    - name: Check if log is large
      ansible.builtin.stat:
        path: /var/log/app/application.log
      register: log_stat

    - name: Rotate log if > 100MB
      when: log_stat.stat.exists and log_stat.stat.size > 104857600
      block:
        - name: Move current log to dated archive
          ansible.builtin.command:
            cmd: "mv /var/log/app/application.log /var/log/app/application.{{ ansible_date_time.iso8601_basic_short }}.log"
          changed_when: true

        - name: Compress rotated log
          ansible.builtin.command:
            cmd: "gzip /var/log/app/application.{{ ansible_date_time.iso8601_basic_short }}.log"
          changed_when: true

        - name: Restart app to create new log
          ansible.builtin.systemd:
            name: myapp
            state: restarted

Complete Playbook: Backup with Timestamp

---
- name: Database backup with timestamped archive
  hosts: dbservers
  gather_facts: true
  become: true
  vars:
    backup_base: /backup/postgres
    backup_name: "pgdump_{{ ansible_date_time.date }}_{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}"
    retention_days: 30
  tasks:
    - name: Create backup directory
      ansible.builtin.file:
        path: "{{ backup_base }}"
        state: directory
        owner: postgres
        group: postgres
        mode: '0700'

    - name: Run PostgreSQL dump
      ansible.builtin.command:
        cmd: "pg_dumpall -f {{ backup_base }}/{{ backup_name }}.sql"
      become_user: postgres
      changed_when: true

    - name: Compress backup
      ansible.builtin.command:
        cmd: "gzip {{ backup_base }}/{{ backup_name }}.sql"
      changed_when: true

    - name: Remove backups older than {{ retention_days }} days
      ansible.builtin.find:
        paths: "{{ backup_base }}"
        patterns: "pgdump_*.sql.gz"
        age: "{{ retention_days }}d"
      register: old_backups

    - name: Delete old backups
      ansible.builtin.file:
        path: "{{ item.path }}"
        state: absent
      loop: "{{ old_backups.files }}"
      when: old_backups.matched > 0

    - name: Log backup completion
      ansible.builtin.lineinfile:
        path: /var/log/backup.log
        line: "{{ ansible_date_time.iso8601 }} | SUCCESS | {{ backup_name }}.sql.gz | {{ inventory_hostname }}"
        create: true
        mode: '0644'

Troubleshooting

ansible_date_time is Undefined

Cause: gather_facts: false is set.

Fix: Either enable fact gathering or use now():

# Option 1: Enable facts
gather_facts: true

# Option 2: Use now() without facts
- debug:
    msg: "{{ now(utc=true).strftime('%Y-%m-%d') }}"

Timestamp Doesn't Change Between Tasks

Cause: ansible_date_time is captured once at play start.

Fix: Use now() for real-time values:

- name: Real-time timestamp (changes each task)
  ansible.builtin.debug:
    msg: "{{ now(utc=true).isoformat() }}"

Timezone Issues

Cause: ansible_date_time.tz reflects the remote host's timezone, not the controller's.

Fix: Always use UTC for consistency:

- name: Force UTC
  ansible.builtin.debug:
    msg: "{{ now(utc=true).strftime('%Y-%m-%dT%H:%M:%SZ') }}"

Conclusion

Ansible provides flexible date/time handling through the ansible_date_time fact (captured at play start) and the now() function with strftime filters (real-time). Use ansible_date_time for consistent timestamps across all tasks in a play, and now() when you need the exact current time at each task. Always prefer UTC for cross-timezone consistency.