Winix.Schedule 0.4.0

Prefix Reserved
dotnet tool install --global Winix.Schedule --version 0.4.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local Winix.Schedule --version 0.4.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=Winix.Schedule&version=0.4.0
                    
nuke :add-package Winix.Schedule --version 0.4.0
                    

schedule

Cross-platform task scheduler with cron expressions.

Unified interface for creating, listing, and managing scheduled tasks using standard cron syntax. On Windows, delegates to schtasks.exe (Windows Task Scheduler). On Linux/macOS, manages crontab entries using tagged comments to track Winix-owned lines non-destructively.

Install

Scoop (Windows)

scoop bucket add winix https://github.com/Yortw/winix
scoop install winix/schedule

Winget (Windows, stable releases)

winget install Winix.Schedule

.NET Tool (cross-platform)

dotnet tool install -g Winix.Schedule

Direct Download

Download native binaries from GitHub Releases.

Usage

schedule <subcommand> [options] [args...]

Plain-text tables, status messages, and diagnostics go to stderr. With --json, the success or error envelope goes to stdout so consumers can do schedule next "..." --json | jq reliably. Usage-error envelopes (e.g. unknown subcommand) also go to stderr. Exit codes and JSON shape are described below.

Subcommands

add

Register a new scheduled task.

# Run dotnet build daily at 2am
schedule add --cron "0 2 * * *" -- dotnet build

# Create a named task running every 5 minutes
schedule add --cron "*/5 * * * *" --name health-check -- curl http://localhost:8080/health

# Run a script with a specific folder
schedule add --cron "0 9 * * 1-5" --folder "\MyApp\" -- powershell -File backup.ps1

list

List scheduled tasks. By default shows only tasks in the \Winix\ folder (Windows) or Winix-tagged crontab entries (Linux/macOS).

# List Winix-managed tasks
schedule list

# List all tasks (including system tasks and other users' tasks)
schedule list --all

remove

Remove a task by name.

schedule remove health-check

enable

Re-enable a disabled task.

schedule enable health-check

disable

Disable a task without removing it. The task definition is preserved; it will not fire until re-enabled.

schedule disable health-check

run

Trigger an immediate (on-demand) execution of a task, independent of its cron schedule.

schedule run health-check

history

Show run history for a task (Windows only; returns a note on Linux/macOS where history is not available via crontab).

schedule history health-check

next

Compute and display upcoming fire times for a cron expression. No backend interaction — pure cron calculation.

# Show next 5 fire times for a cron expression
schedule next "0 2 * * *"

# Show next 10 fire times
schedule next "*/5 * * * *" --count 10

Cron Expression Syntax

Five space-separated fields: minute hour day-of-month month day-of-week

Field Range Special characters
Minute 0–59 * , - /
Hour 0–23 * , - /
Day of month 1–31 * , - /
Month 1–12 * , - /
Day of week 0–6 (0=Sun) * , - /

Special values

Expression Meaning
* Every value
*/N Every N-th value (step)
1,3,5 Specific values (list)
1-5 Inclusive range

Examples

Expression Fires
0 2 * * * Daily at 2:00am
*/5 * * * * Every 5 minutes
0 9 * * 1-5 Weekdays at 9:00am
30 6 1 * * 1st of every month at 6:30am
0 0 * * 0 Every Sunday at midnight

Folder Scoping

On Windows, tasks are created in the \Winix\ folder by default. This prevents accidental modification of system tasks or tasks created by other tools. Use --folder to specify a different folder.

On Linux/macOS, Winix-managed crontab entries are tagged with a # winix:<name> comment. list shows only tagged entries by default; --all shows the full crontab.

Option Description
(default) Windows: \Winix\ folder. Linux/macOS: Winix-tagged entries only.
--folder PATH Use the specified Task Scheduler folder (Windows only).
--all Include all tasks, not just Winix-managed ones.

Options

Option Description
--cron EXPR Cron expression (required for add).
--name NAME Task name. Auto-generated from the command if omitted.
--folder PATH Task Scheduler folder (Windows). Default: \Winix\.
--count N Number of fire times to show for next (default: 5).
--all Show all tasks, not just Winix-managed. Used with list.
--json Output results as a JSON envelope on stdout (usage-error envelopes go to stderr).
--color[=auto\|always\|never] Coloured output: auto (default when omitted), always, or never.
--no-color Disable coloured output.
--help Show help and exit.
--version Show version and exit.

Exit Codes

Code Meaning
0 Success.
125 Usage error — bad arguments or invalid cron expression.
126 Backend failure — task not found, schtasks/crontab returned non-zero, or scheduler unavailable.

Platform Notes

Platform Implementation
Windows Delegates to schtasks.exe, which is present on all Windows versions since XP. Creates tasks in \Winix\ by default. Run history via Task Scheduler event log.
Linux / macOS Manages crontab -e entries. Winix-owned lines are tagged with # winix:<name> to enable non-destructive add/remove. History is not available via crontab.

Colour

  • Table headers are dimmed in list output for visual separation from the data rows.
  • Status indicators ( / ) returned from add/remove/run/enable/disable operations are coloured (green for success, red for failure).
  • --no-color suppresses all ANSI colour output.
  • Respects the NO_COLOR environment variable (no-color.org).

Part of Winix

schedule is part of the Winix CLI toolkit.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.4.0 129 6/13/2026
0.3.0 115 5/26/2026
0.3.0-rc2 120 5/10/2026
0.2.0 127 4/16/2026
0.2.0-test4 112 4/15/2026

## [0.3.0] - 2026-05-10

### Changed (BREAKING)
- Tool-error exit codes now follow the suite-wide POSIX convention (`125` usage / `126` non-runnable / `127` not-found). The legacy generic `1` exit on tool-side errors has been removed; child-process exit codes still pass through unchanged.
- `--json` envelopes for happy-path commands (`add`, `remove`, `list`, `dryrun`, `next`, `history`) now route to **stdout** instead of stderr, per the suite-wide JSON-routing convention. Pre-fix `schedule add … --json | jq` saw an empty pipe because the success envelope was on the wrong stream. Error envelopes still go to stderr (matching gh / kubectl / AWS CLI / jq).
- Cron expressions that have no clean schtasks mapping (e.g. step values, mixed-list day-of-week, complex ranges Windows Task Scheduler can't represent) are now rejected at `add` time rather than being silently degraded into a different schedule on Windows.

### Security
- `schedule add` arguments destined for `schtasks /TR` are now escaped per the Microsoft CRT argument-parsing rules. Pre-fix a command containing `"` or trailing `\` could splice additional tokens into the scheduled action, allowing unintended commands to run on the trigger.
- Crontab argument quoting widened to cover the full POSIX shell metacharacter set (`;`, `&`, `|`, `` ` ``, `$`, `(`, `)`, `<`, `>`, `*`, `?`, `[`, `]`, `~`, `#`, whitespace). Pre-fix a command argument containing one of these characters was written unquoted into the crontab line and reinterpreted by `/bin/sh` at trigger time.
- `schedule add` rejects newline characters in `--name`, command, and arguments. Pre-fix an embedded `\n` would split into a second crontab entry or a second schtasks command line, allowing injection of an additional scheduled task the user never authorised.
- Detached `run` invocations are now wrapped in shell braces so a compound target (`a && b`) cannot escape the foreground/background terminator boundary.

### Fixed
- `crontab -l` failure modes are now distinguished. Pre-fix any non-zero exit from `crontab -l` was treated as "user has no crontab" and `schedule list` returned an empty result; if `schedule add` then ran, the resulting write would silently overwrite a real crontab the tool failed to read. Real failures (cron.deny, PAM, locked spool, missing binary) now surface as `Unavailable` with the underlying stderr text and exit `126`.
- `CrontabParser.AddEntry` for a name that already exists now overwrites the previous entry (matching `schtasks /F` semantics) instead of appending a duplicate. Pre-fix a re-`add` produced two entries with the same name; subsequent `remove` only deleted one of them.
- DST drift in `CronExpression.GetNextOccurrence`. Pre-fix the calculation captured a single UTC offset and reused it across iterations, so the returned wall-clock time was wrong for ~6 months of every year in any DST zone (NZ, AU, EU, US). Now performs wall-clock arithmetic in the target `TimeZoneInfo` with per-candidate offset recompute; spring-forward gap is skipped via `IsInvalidTime`, fall-back uses the .NET default (first occurrence).
- `SchtasksCsvParser` no longer fails on non-en-US date formats. Pre-fix the parser used the invariant culture (`InvariantGlobalization=true` is the AOT default) so dates like `15/06/2026 09:00:00` from a UK / NZ / DE / FR Windows host failed to parse, dropping rows from `schedule list`. Parser now tries the host's regional date format with invariant fallback.
- `schedule list` now surfaces backend failures as a diagnostic warning + exit `126` rather than returning an empty result. Pre-fix an authentication failure, cron daemon error, or trailing-backslash `/TN` query (`\Winix\` on Windows produced "filename syntax is incorrect") was indistinguishable from "no scheduled tasks".
- `schedule next` no longer crashes on unsatisfiable cron expressions (e.g. `0 0 30 2 *`). The `InvalidOperationException` from the underlying iterator is caught and surfaced as a clean diagnostic.
- `schedule run` rejects disabled tasks with a clear "is disabled" message and exit `126`. Pre-fix the disabled gate was bypassed for crontab entries.
- Crontab line lifecycle (`add` / `remove` / `enable` / `disable`) now handles blank-line spacing consistently. Pre-fix a mid-cycle operation against a crontab with readability blank lines could collapse two adjacent entries onto one line, corrupting the schedule.
- `schtasks` invocations that report `ERROR_ELEVATION_REQUIRED` now surface a UAC elevation hint in the error message, instead of a bare exit code.
- Stderr output that arrives on a successful exit is now surfaced as a warning rather than discarded. Pre-fix partial-failure cases (e.g. a single bad row mid-listing) succeeded silently.
- Crontab partial-write failures (write succeeds but the daemon rejects on reload) now produce a specific error message identifying the partial-write state.
- Backend pipe deadlocks fixed in both `schtasks` and `crontab` paths. Pre-fix a child producing more than one OS pipe buffer of stderr while we read only stdout could hang the tool indefinitely; both streams are now drained concurrently with a timeout, and unhandled `Win32Exception` from missing binaries widened to a clean diagnostic.
- `crontab` `run` no longer leaks the child's stdio into the parent's terminal during read.
- Stderr writes guarded against `IOException` (broken pipe) and `ObjectDisposedException` so a closed downstream pipe can't crash the tool mid-emit.
- `--version` output no longer carries the `+gitsha` SourceLink suffix; users now see plain `schedule 0.3.0`, matching the suite-wide convention.

### Added
- `--describe` JSON now advertises the full per-mode field schema. Pre-fix several emitted fields (warning text, failure reason on list, history availability flag) were undocumented, breaking the introspection contract for AI agents and automation.
- Manual page (`man 1 schedule`) now ships with the package and is installed to `share/man/man1/` by scoop and the native installer.

### Internal
- Eight error-message formatters extracted as pure helpers (`FormatRunFailureNullProcess`, `FormatShExit`, `FormatShUnavailable`, `FormatGenericFailure`, `FormatWriteTimeout`, `FormatWriteFailure`, `SchtasksBackend.FormatLaunchFailure`, `FormatTimeoutFailure`) so the message-format paths inside Process-spawning methods are now unit-testable.
- `ScheduleListResult { Available, Tasks, Warning, FailureReason }` widens the list-return shape; `SchtasksBackend` distinguishes benign-empty from real failure via `IsBenignSchtasksEmpty`; `CrontabBackend` propagates `CrontabUnavailableException` as `Unavailable`.
- `CronExpression.GetNextOccurrence` now exposes an internal overload taking `TimeZoneInfo` to allow host-independent DST tests; public API still defaults to `TimeZoneInfo.Local`.

### Documentation
- README, `man 1 schedule`, and the AI agent guide updated to reflect the success-envelope-on-stdout / error-envelope-on-stderr split, the new exit-code conventions, the `Unavailable` list semantics, and the security-relevant escaping rules.
- README colour section corrected to match `Formatting.FormatTable` actual behaviour (pre-fix the documented colour mapping had drifted from the implementation).

See full changelog at https://github.com/Yortw/winix/blob/main/src/schedule/CHANGELOG.md