# "Time & Duration"
_Path: en/lua/core/time_
> "Work with time values, durations, timezones, and scheduling. Create timers, sleep for specified periods, parse and format timestamps."
## Table of Contents
- Time & Duration
## Content
# Time & Duration
The `time` module provides time values, durations, time-zone handling, parsing, formatting, sleeps, and timers. Supported workflow time calls are recorded so they can replay deterministically.
This is an API reference. Code blocks are isolated examples or partial scheduling patterns, not a complete entry. Names such as `do_work`, `try_operation`, `make_request`, `send_reminder`, `user_activity`, `check_health`, and `process` represent application callbacks, channels, or data. Where a snippet assigns an error return to `_`, it assumes the shown literal is valid; handle errors when values can come from input or configuration.
## Loading
```lua
local time = require("time")
```
Add `time` to the executable entry's `modules:` list before requiring it. The ambient `channel` and `errors` globals used by scheduling examples need no module declaration.
### `now`
Returns the current time. In workflows, it returns the recorded workflow time reference so execution can replay deterministically.
```lua
local t = time.now()
print(t:format_rfc3339()) -- "2024-12-29T15:04:05Z"
-- Measure elapsed time
local start = time.now()
do_work()
local elapsed = time.now():sub(start)
print("Took " .. elapsed:milliseconds() .. "ms")
```
The timestamp and elapsed-time output are illustrative; `time.now()` supplies the current or recorded workflow time.
**Returns:** `Time`
### Create from Components
```lua
-- Create specific date/time in UTC
local t = time.date(2024, time.DECEMBER, 25, 10, 30, 0, 0, time.utc)
print(t:format_rfc3339()) -- "2024-12-25T10:30:00Z"
-- Create in specific timezone
local ny, err = time.load_location("America/New_York")
if err then
return nil, err
end
local meeting = time.date(2024, time.JANUARY, 15, 14, 0, 0, 0, ny)
-- Defaults to local timezone if not specified
local t = time.date(2024, 1, 15, 12, 0, 0, 0)
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `year` | number | Year |
| `month` | number | Month (1-12 or `time.JANUARY` etc) |
| `day` | number | Day of month |
| `hour` | number | Hour (0-23) |
| `minute` | number | Minute (0-59) |
| `second` | number | Second (0-59) |
| `nanosecond` | number | Nanosecond (0-999999999) |
| `location` | Location | Timezone (optional, defaults to local) |
**Returns:** `Time`
### Create from a Unix Timestamp
```lua
-- From seconds since epoch
local t = time.unix(1703862245, 0)
print(t:utc():format_rfc3339()) -- "2023-12-29T15:04:05Z"
-- With nanoseconds
local t = time.unix(1703862245, 500000000) -- +500ms
-- Convert JavaScript timestamp (milliseconds)
local js_timestamp = 1703862245000
local t = time.unix(js_timestamp // 1000, (js_timestamp % 1000) * 1000000)
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `sec` | number | Unix seconds |
| `nsec` | number | Nanoseconds offset |
**Returns:** `Time`
### Parse from a String
Parse time strings using Go's reference time format: `Mon Jan 2 15:04:05 MST 2006`.
```lua
-- Parse RFC3339
local t, err = time.parse(time.RFC3339, "2024-12-29T15:04:05Z")
if err then
return nil, err
end
-- Parse custom format
local t, err = time.parse("2006-01-02", "2024-12-29")
local t, err = time.parse("15:04:05", "14:30:00")
local t, err = time.parse("2006-01-02 15:04:05 MST", "2024-12-29 14:30:00 EST")
-- Parse in specific timezone
local ny, _ = time.load_location("America/New_York")
local t, err = time.parse("2006-01-02 15:04", "2024-12-29 14:30", ny)
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `layout` | string | Go time format layout |
| `value` | string | String to parse |
| `location` | Location | Default time zone (optional) |
**Returns:** `Time, error`
### Arithmetic
```lua
local t = time.now()
-- Add duration (accepts number, string, or Duration)
local tomorrow = t:add("24h")
local later = t:add(5 * time.MINUTE)
local d, _ = time.parse_duration("1h30m")
local future = t:add(d)
-- Subtract time to get duration
local diff = tomorrow:sub(t) -- returns Duration
print(diff:hours()) -- 24
-- Add calendar units (handles month boundaries correctly)
local next_month = t:add_date(0, 1, 0) -- add 1 month
local next_year = t:add_date(1, 0, 0) -- add 1 year
local last_week = t:add_date(0, 0, -7) -- subtract 7 days
```
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| `add(duration)` | number/string/Duration | Time | Add duration |
| `sub(time)` | Time | Duration | Difference between times |
| `add_date(years, months, days)` | numbers | Time | Add calendar units |
### Comparison
```lua
local t1 = time.date(2024, 1, 1, 0, 0, 0, 0, time.utc)
local t2 = time.date(2024, 1, 2, 0, 0, 0, 0, time.utc)
t1:before(t2) -- true
t2:after(t1) -- true
t1:equal(t1) -- true
```
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| `before(time)` | Time | boolean | Whether this time is before the other value |
| `after(time)` | Time | boolean | Whether this time is after the other value |
| `equal(time)` | Time | boolean | Whether the two values represent the same time |
### Formatting
```lua
local t = time.now()
t:format_rfc3339() -- "2024-12-29T15:04:05Z"
t:format(time.DATE_ONLY) -- "2024-12-29"
t:format(time.TIME_ONLY) -- "15:04:05"
t:format("Mon Jan 2, 2006") -- "Sun Dec 29, 2024"
```
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| `format(layout)` | string | string | Format using Go layout |
| `format_rfc3339()` | - | string | Format as RFC3339 |
### Unix Timestamps
```lua
local t = time.now()
t:unix() -- seconds since epoch
t:unix_nano() -- nanoseconds since epoch
```
### Components
```lua
local t = time.now()
-- Get date parts
local year, month, day = t:date()
-- Get time parts
local hour, min, sec = t:clock()
-- Individual accessors
t:year() -- e.g., 2024
t:month() -- 1-12
t:day() -- 1-31
t:hour() -- 0-23
t:minute() -- 0-59
t:second() -- 0-59
t:nanosecond() -- 0-999999999
t:weekday() -- 0=Sunday .. 6=Saturday
t:year_day() -- 1-366
t:is_zero() -- true if zero value
```
### Time-Zone Conversion
```lua
local t = time.now()
t:utc() -- convert to UTC
t:in_local() -- convert to local timezone
t:in_location(ny) -- convert to specific timezone
t:location() -- get current Location
t:location():string() -- get timezone name
```
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| `utc()` | - | Time | Convert to UTC |
| `in_local()` | - | Time | Convert to the local time zone |
| `in_location(loc)` | Location | Time | Convert to a specified time zone |
| `location()` | - | Location | Return the current time zone |
### Rounding
Round or truncate to duration boundaries. **Requires Duration userdata** (not number or string).
```lua
local t = time.now()
local hour_duration, _ = time.parse_duration("1h")
local minute_duration, _ = time.parse_duration("15m")
t:round(hour_duration) -- round to nearest hour
t:truncate(minute_duration) -- truncate to 15-minute boundary
```
| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| `round(duration)` | Duration | Time | Round to nearest multiple |
| `truncate(duration)` | Duration | Time | Truncate to multiple |
### Create a Duration
```lua
-- Parse from string
local d, err = time.parse_duration("1h30m45s")
local d, err = time.parse_duration("500ms")
local d, err = time.parse_duration("2h30m45s500ms")
-- From number (nanoseconds)
local d, err = time.parse_duration(time.SECOND)
local d, err = time.parse_duration(5 * time.MINUTE)
-- Valid units: ns, us, ms, s, m, h
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `value` | number/string/Duration | Duration to parse |
**Returns:** `Duration, error`
### Duration Methods
```lua
local d, _ = time.parse_duration("1h30m45s500ms")
d:hours() -- 1.5126...
d:minutes() -- 90.75...
d:seconds() -- 5445.5
d:milliseconds() -- 5445500
d:microseconds() -- 5445500000
d:nanoseconds() -- 5445500000000
```
### Named Locations
Load a time zone by its IANA name, such as `America/New_York`, `Europe/London`, or `Asia/Tokyo`.
```lua
local ny, err = time.load_location("America/New_York")
if err then
return nil, err
end
local tokyo, _ = time.load_location("Asia/Tokyo")
local london, _ = time.load_location("Europe/London")
-- Convert between timezones
local t = time.now():utc()
print("UTC:", t:format(time.TIME_ONLY))
print("New York:", t:in_location(ny):format(time.TIME_ONLY))
print("Tokyo:", t:in_location(tokyo):format(time.TIME_ONLY))
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `name` | string | IANA time-zone name |
**Returns:** `Location, error`
### Fixed-Offset Locations
Create a time zone with a fixed UTC offset.
```lua
-- UTC+5:30 (India Standard Time)
local ist = time.fixed_zone("IST", 5*3600 + 30*60)
-- UTC-8 (Pacific Standard Time)
local pst = time.fixed_zone("PST", -8*3600)
local t = time.date(2024, 1, 15, 12, 0, 0, 0, ist)
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `name` | string | Zone name |
| `offset` | number | UTC offset in seconds |
**Returns:** `Location`
### Built-In Locations
```lua
time.utc -- UTC timezone
time.localtz -- Local system timezone
```
### `sleep`
Suspend execution for the specified duration. Workflow execution records the sleep for deterministic replay.
```lua
time.sleep("5s")
time.sleep(500 * time.MILLISECOND)
-- Backoff pattern
for attempt = 1, 3 do
local ok = try_operation()
if ok then break end
time.sleep(tostring(attempt) .. "s")
end
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `duration` | number/string/Duration | Sleep time |
### `after`
Returns a channel that receives one value after the duration. The channel can be used with `channel.select`.
```lua
-- Simple timeout
local timeout, err = time.after("5s")
if err then return nil, err end
timeout:receive() -- blocks for 5 seconds
-- Timeout with select
local response_ch = make_request()
local timeout_ch, err = time.after("30s")
if err then return nil, err end
local result = channel.select{
response_ch:case_receive(),
timeout_ch:case_receive()
}
if result.channel == timeout_ch then
return nil, errors.new({message = "Request timed out", kind = errors.TIMEOUT})
end
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `duration` | number/string/Duration | Time to wait |
**Returns:** `Channel, error`
### `timer`
Creates a one-shot timer that fires after the specified duration and can be stopped or reset.
```lua
local timer, err = time.timer("5s")
if err then
return nil, err
end
-- Wait for timer
timer:response():receive()
send_reminder()
-- Reset on activity
local idle_timer, err = time.timer("5m")
if err then
return nil, err
end
local idle_ch = idle_timer:response()
while true do
local r = channel.select{
user_activity:case_receive(),
idle_ch:case_receive()
}
if r.channel == idle_ch then
logout_user()
break
end
idle_timer:reset("5m")
end
-- Stop timer
timer:stop()
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `duration` | number/string/Duration | Time until fire |
**Returns:** `Timer, error`
| Timer Method | Parameters | Returns | Description |
|--------------|------------|---------|-------------|
| `response()` | - | Channel | Get timer channel |
| `channel()` | - | Channel | Alias for response() |
| `stop()` | - | boolean | Cancel timer |
| `reset(duration)` | number/string/Duration | boolean | Reset with new duration |
### `ticker`
Creates a repeating timer that fires at regular intervals.
```lua
-- Periodic task
local ticker, err = time.ticker("30s")
if err then
return nil, err
end
local ch = ticker:response()
while true do
local tick_time = ch:receive()
check_health()
end
```
The loop above is intended for a long-running process. A separate finite rate-limiting pattern is:
```lua
-- Rate limiting
local ticker, err = time.ticker("100ms")
if err then
return nil, err
end
for _, item in ipairs(items) do
ticker:response():receive()
process(item)
end
ticker:stop()
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `duration` | number/string/Duration | Interval between ticks |
**Returns:** `Ticker, error`
| Ticker Method | Parameters | Returns | Description |
|---------------|------------|---------|-------------|
| `response()` | - | Channel | Get ticker channel |
| `channel()` | - | Channel | Alias for response() |
| `stop()` | - | boolean | Stop ticker |
### Duration Units
Duration constants are expressed in nanoseconds and can be combined with arithmetic.
```lua
time.NANOSECOND -- 1
time.MICROSECOND -- 1,000
time.MILLISECOND -- 1,000,000
time.SECOND -- 1,000,000,000
time.MINUTE -- 60 * SECOND
time.HOUR -- 60 * MINUTE
-- Example usage
time.sleep(5 * time.SECOND)
local timeout, err = time.after(30 * time.SECOND)
if err then return nil, err end
```
### Format Layouts
```lua
time.RFC3339 -- "2006-01-02T15:04:05Z07:00"
time.RFC3339NANO -- "2006-01-02T15:04:05.999999999Z07:00"
time.RFC822 -- "02 Jan 06 15:04 MST"
time.RFC822Z -- "02 Jan 06 15:04 -0700"
time.RFC850 -- "Monday, 02-Jan-06 15:04:05 MST"
time.RFC1123 -- "Mon, 02 Jan 2006 15:04:05 MST"
time.RFC1123Z -- "Mon, 02 Jan 2006 15:04:05 -0700"
time.DATE_TIME -- "2006-01-02 15:04:05"
time.DATE_ONLY -- "2006-01-02"
time.TIME_ONLY -- "15:04:05"
time.KITCHEN -- "3:04PM"
time.STAMP -- "Jan _2 15:04:05"
time.STAMP_MILLI -- "Jan _2 15:04:05.000"
time.STAMP_MICRO -- "Jan _2 15:04:05.000000"
time.STAMP_NANO -- "Jan _2 15:04:05.000000000"
```
### Months
```lua
time.JANUARY -- 1
time.FEBRUARY -- 2
time.MARCH -- 3
time.APRIL -- 4
time.MAY -- 5
time.JUNE -- 6
time.JULY -- 7
time.AUGUST -- 8
time.SEPTEMBER -- 9
time.OCTOBER -- 10
time.NOVEMBER -- 11
time.DECEMBER -- 12
```
### Weekdays
```lua
time.SUNDAY -- 0
time.MONDAY -- 1
time.TUESDAY -- 2
time.WEDNESDAY -- 3
time.THURSDAY -- 4
time.FRIDAY -- 5
time.SATURDAY -- 6
```
## Errors
| Condition | Kind | Retryable |
|-----------|------|-----------|
| Invalid duration format | `errors.INVALID` | no |
| Parse failed | `errors.INVALID` | no |
| Empty location name | `errors.INVALID` | no |
| Location not found | `errors.NOT_FOUND` | no |
| Duration <= 0 (timer/ticker) | `errors.INVALID` | no |
```lua
local t, err = time.parse(time.RFC3339, "invalid")
if err then
if errors.is(err, errors.INVALID) then
print("Invalid format:", err:message())
end
return nil, err
end
local loc, err = time.load_location("Unknown/Zone")
if err then
if errors.is(err, errors.NOT_FOUND) then
print("Location not found:", err:message())
end
return nil, err
end
```
See [Error Handling](lua/core/errors.md) for working with errors.
## Navigation
Previous: "Errors" (lua/core/errors)
Next: "Channels and Coroutines" (lua/core/channel)