Winix.Schedule
0.4.0
Prefix Reserved
dotnet tool install --global Winix.Schedule --version 0.4.0
dotnet new tool-manifest
dotnet tool install --local Winix.Schedule --version 0.4.0
#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
listoutput 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-colorsuppresses all ANSI colour output.- Respects the
NO_COLORenvironment variable (no-color.org).
Part of Winix
schedule is part of the Winix CLI toolkit.
| Product | Versions 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. |
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