日本語 · English

Kairos for Windows administrators — PowerShell and Task Scheduler, nothing else

Translated from the canonical Japanese page recipes/windows-admin.md.

A page for Windows system administrators: people who use PowerShell and Task Scheduler but do not write software. You do not need to read the language specification. This page alone takes you from “write a definition” to “ask whether today is a firing day” to “ask every morning, automatically” to “put it in Outlook”.

The example is the definition from Patch Tuesday — the second Tuesday at 10:00 Pacific Time. On the Tokyo calendar that is early Wednesday morning, and in April and July it is the third Wednesday.

Three ways in

The first run — write a definition, ask with list

Save the following as C:\kairos\patch.kairos (the # eval: and #=> lines are verification comments for this site; you may delete them):

# Patch Tuesday (second Tuesday, 10:00 Pacific Time)
# eval: 2026-10-01..2026-12-31 tz: Asia/Tokyo
premise PT {
  calendar-system: Gregorian
  tz: "America/Los_Angeles"
  wkst: Sun
}
@PT
patchDay = everyDay |> filter(d => weekday(d) == Tue) |> within(month) |> nth(2)
patchDay |> at(T10:00)
#=> 2026-10-14T02:00 2026-11-11T03:00 2026-12-09T03:00

Ask from PowerShell:

kairos list --from 2026-10-01 --to 2026-12-31 --tz Asia/Tokyo C:\kairos\patch.kairos
2026-10-14T02:00
2026-11-11T03:00
2026-12-09T03:00

Reading the output:

Definitions that carry a holiday table may add annotation lines starting with # (this example has no table, so none appear):

2027-01-25
2027-02-25
# ⚠ 範囲外 2027-01-01..2027-03-01(holidays2026 covering 2026-01-01..2026-12-31)
# 被覆サマリ
#   holidays2026 covering 2026-01-01..2026-12-31 残走路 -59 日

Receiving it in PowerShell — --json

For decisions and further processing, ask with --json and turn it into objects with ConvertFrom-Json:

$r = kairos list --from 2026-10-01 --to 2026-12-31 --tz Asia/Tokyo --json C:\kairos\patch.kairos | ConvertFrom-Json
$r.results[0].dates     # 2026-10-14T02:00 2026-11-11T03:00 2026-12-09T03:00
$r.results[0].points    # the same points in epoch milliseconds (1791910800000 …)
$r.coverage             # holiday-table coverage and runway (empty when there is no table)

The character-encoding trap. Do not use > to write to a file:

Asking “is today a firing day?” every morning — Task Scheduler

One pattern. Keep Task Scheduler as the clock, and move only the decision to Kairos. Every morning at 08:30, ask “is there a point today?” and, if so, start the notification script. The same pattern for cron is in the “Running it” section of Last day of the month.

The decision script, C:\kairos\run-if-today.ps1:

# run-if-today.ps1 — if today is a firing day, run the command given in the remaining arguments (Windows PowerShell 5.1 or later)
# Usage: powershell -NoProfile -ExecutionPolicy Bypass -File run-if-today.ps1 <definition.kairos> <command to run> [args...]
# Rehearsal: set the environment variable KAIROS_TODAY to YYYY-MM-DD and that day is treated as "today"
param(
  [Parameter(Mandatory = $true)] [string] $Definition,
  [Parameter(Mandatory = $true)] [string] $Command,
  [Parameter(ValueFromRemainingArguments = $true)] [string[]] $CommandArgs = @()
)
$kairos = 'kairos'                      # when running as SYSTEM, use the absolute path of kairos.cmd under C:\kairos (the administrator's pattern)
$tz = 'Asia/Tokyo'                      # whose calendar "today" is read on; keep it equal to the machine's time-zone setting
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8   # kairos writes UTF-8

$culture = [System.Globalization.CultureInfo]::InvariantCulture
$today = if ($env:KAIROS_TODAY) { $env:KAIROS_TODAY } else { (Get-Date).ToString('yyyy-MM-dd', $culture) }
$tomorrow = [datetime]::ParseExact($today, 'yyyy-MM-dd', $culture).AddDays(1).ToString('yyyy-MM-dd', $culture)

# Ask --json for the window from today up to, but not including, tomorrow
$json = & $kairos list --from $today --to $tomorrow --tz $tz --json $Definition
if ($LASTEXITCODE -ne 0) {
  [Console]::Error.WriteLine("kairos failed (exit code $LASTEXITCODE): $json")
  exit 1
}
$report = ($json -join "`n") | ConvertFrom-Json
if (-not $report) { exit 1 }            # unreadable JSON: do nothing
$dates = @($report.results[0].dates | Where-Object { $_ })
if ($dates.Count -eq 0) { exit 0 }      # not a firing day: exit quietly

& $Command @CommandArgs                 # firing day: run the notification script
exit $LASTEXITCODE

Rehearse first. Hand it a date through the environment variable and run it by hand. Nothing happens on 10/13 (Tuesday); the notification runs only on 10/14 (Wednesday):

$env:KAIROS_TODAY = '2026-10-13'
powershell -NoProfile -ExecutionPolicy Bypass -File C:\kairos\run-if-today.ps1 C:\kairos\patch.kairos C:\kairos\notify.ps1
$env:KAIROS_TODAY = '2026-10-14'
powershell -NoProfile -ExecutionPolicy Bypass -File C:\kairos\run-if-today.ps1 C:\kairos\patch.kairos C:\kairos\notify.ps1
Remove-Item Env:KAIROS_TODAY

Register the task. Decide every day at 08:30 (from an administrator PowerShell or Command Prompt):

schtasks /Create /SC DAILY /ST 08:30 /TN "Kairos\PatchTuesday" /TR "powershell -NoProfile -ExecutionPolicy Bypass -File C:\kairos\run-if-today.ps1 C:\kairos\patch.kairos C:\kairos\notify.ps1"

Run-as account and power (measured 2026-10-13):

To try it right away: schtasks /Run /TN "Kairos\PatchTuesday". The result is “Last Result” in schtasks /Query /TN "Kairos\PatchTuesday" /V /FO LIST (0 is success, 1 is an error).

The administrator’s pattern — as SYSTEM, on a machine that stays on

Notifications should go out whether or not anyone is logged on. Register the task as SYSTEM on a machine that is always running (a server, say) — measured 2026-10-13.

Prerequisites:

npm install --prefix C:\kairos kairos-lang
# change this one line in run-if-today.ps1
$kairos = 'C:\kairos\node_modules\.bin\kairos.cmd'

Register it (in an administrator PowerShell, using the ScheduledTasks module instead of schtasks):

$task = @{ TaskName = 'PatchTuesday'; TaskPath = '\Kairos\' }
$argument = '-NoProfile -ExecutionPolicy Bypass -File "C:\kairos\run-if-today.ps1" -Definition "C:\kairos\patch.kairos" -Command "C:\kairos\notify.ps1"'

Get-ScheduledTask @task -ErrorAction Ignore | Unregister-ScheduledTask -Confirm:$false
Register-ScheduledTask @task `
  -Action    (New-ScheduledTaskAction -Execute '%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe' -Argument $argument) `
  -Trigger   (New-ScheduledTaskTrigger -Daily -At '08:30') `
  -Principal (New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -LogonType ServiceAccount) `
  -Settings  (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -DontStopOnIdleEnd)
$argument = '-NoProfile -ExecutionPolicy Bypass -Command $env:KAIROS_TODAY = ''2026-10-14''; . "C:\kairos\run-if-today.ps1" -Definition "C:\kairos\patch.kairos" -Command "C:\kairos\notify.ps1"'

Putting it in a calendar — Outlook

Export an iCalendar file with --ics and import it into Outlook. Write the file through cmd /c (the encoding trap above):

cmd /c "kairos list --from 2026-10-01 --to 2027-01-01 --tz Asia/Tokyo --ics C:\kairos\patch.kairos > C:\kairos\patch.ics"

What is in the file:

Importing into Outlook (measured 2026-10-13 in the new Outlook and in classic Outlook):

Outlook cannot import --ics-series (measured the same day).

To get the reminder in the morning, move the time in the definition. Keep the release point inside the Pacific Time premise, refer to it from a Tokyo premise, and attach 9:00 of that day:

# Patch Tuesday notification (9:00 in Tokyo)
# eval: 2026-10-01..2027-01-01 tz: Asia/Tokyo
premise PT {
  calendar-system: Gregorian
  tz: "America/Los_Angeles"
  wkst: Sun
  release = everyDay |> filter(d => weekday(d) == Tue) |> within(month) |> nth(2) |> at(T10:00)
}
premise JP {
  calendar-system: Gregorian
  tz: "Asia/Tokyo"
  wkst: Mon
}
@JP
PT.release |> snapTo(day) |> at(T09:00)
#=> 2026-10-14T09:00 2026-11-11T09:00 2026-12-09T09:00

Pitfalls at a glance

Pitfall What happens What to do
No --tz Points print in the machine’s time zone. On a UTC server that is Tuesday 17:00 Always --tz Asia/Tokyo. The definition’s tz and the display tz are two different things
> in PowerShell 5.1 The file is UTF-16 and other tools cannot read it cmd /c "kairos … > out.json" — works in every version
Garbled non-ASCII text PowerShell decodes UTF-8 with its default code page Run [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 first
Execution policy The .ps1 stops with “running scripts is disabled” powershell -ExecutionPolicy Bypass -File … — affects that process only
The task’s user Without /RU it is the registering account and runs only while logged on. SYSTEM cannot use a kairos in a user’s folder To run always, use the administrator’s pattern (SYSTEM, installed under C:\kairos, absolute path)
--ics-series into Outlook New Outlook shows an error; classic Outlook silently imports nothing Use --ics
A time in --from/--to The error “日付は YYYY-MM-DD” (dates only) Write dates only. The --to day is not included
Holiday table runs out # ⚠ 範囲外 appears and the runway is negative. Points come out without holidays Extend the table, including the end of covering:
Laptop on battery By default the task does not start unless on AC power, so that day’s notification is silently skipped Clear “only if the computer is on AC power” on the task’s Conditions tab
/SD in schtasks Fails depending on the regional date format Do not use it. /SC DAILY /ST alone starts today
No task output kept Task Scheduler does not save standard output Read the exit code in “Last Result”. If you need a record, write it from the notification script

Next version

The next version adds a tab-separated table output for Excel.

Try it in your browser

Open the definition in the Playground (shown in Tokyo time) — the three points above come out. Change the range or at(T10:00) and try. Everything runs in the browser; nothing is sent anywhere.