Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@
{
"title": "Linux (experimental)",
"href": "/linux"
},
"title": "Windows (Task Scheduler)",
"href": "/windows" },
{
"title": "Troubleshooting",
"href": "/troubleshooting"
Expand Down
136 changes: 136 additions & 0 deletions docs/windows.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
title: "Windows (Task Scheduler)"
description: Task Scheduler based background execution on Windows
---

Windows support is provided by the `workmanager_windows` package. Background
tasks are registered as **per-user Task Scheduler tasks** via the `schtasks`
command line; when a task fires, Windows launches your app executable with a
`--background-task <taskName>` argument, the app runs the registered callback
handler headless and exits. v1 is pure Dart — no COM, no C++ and no `win32`
dependency.

> ⚠️ **EXPERIMENTAL.** Read [Honest limitations](#honest-limitations) before
> using this package.

## Setup

### 1. Add the dependency

```yaml
dependencies:
workmanager_windows: ^0.0.1
```

The main `workmanager` package delegates to this package on Windows, so
`Workmanager()` works there too.

### 2. Make `main()` headless-aware

The Task Scheduler launches the app with `--background-task`, so `main()`
must detect that argument and run the dispatcher instead of the UI:

```dart
import 'package:workmanager/workmanager.dart';
import 'package:workmanager_windows/workmanager_windows.dart';

@pragma('vm:entry-point')
void callbackDispatcher() {
WorkmanagerWindows().executeTask((taskName, inputData) async {
// Do your background work here. The full Flutter engine is available,
// but no window is shown. Return true on success — the return value
// becomes the process exit code Task Scheduler records as the task's
// last result.
return true;
});
}

void main(List<String> args) {
if (WorkmanagerWindows.maybeRunBackgroundTask(args, callbackDispatcher)) {
return;
}
Workmanager().initialize(callbackDispatcher);
runApp(const MyApp());
}
```

The headless path runs the dispatcher, invokes the registered handler with
the persisted payload, logs the result and exits. The dispatcher must
register its handler through `WorkmanagerWindows().executeTask(...)` (or
`WorkmanagerExecution.instance.executeTask(...)`), not
`Workmanager().executeTask(...)` — on Windows the handler runs in a separate
process, so it is registered in the shared execution registry instead of the
main package's in-process handler slot.

### 3. Register tasks as usual

```dart
final workmanager = Workmanager();
await workmanager.registerOneOffTask(
'unique-name',
'taskName',
inputData: {'key': 'value'},
initialDelay: const Duration(minutes: 5),
);
await workmanager.registerPeriodicTask(
'unique-name',
'taskName',
frequency: const Duration(minutes: 30),
);
```

## Supported surface

- **One-off tasks** — `schtasks /Create /SC ONCE` at `now + initialDelay`.
- **Periodic tasks** — `schtasks /Create /SC DAILY /RI <minutes>`; the
frequency is clamped to 1 minute..416 days.
- **`inputData`** — persisted as a JSON file under
`%LOCALAPPDATA%\workmanager_windows\payloads\<uniqueName>.json` and passed
to the headless process via `--payload-file`.
- **`cancelByUniqueName` / `cancelAll`** — `schtasks /End` (running
instance) + `schtasks /Delete` + payload cleanup.
- **`isScheduledByUniqueName`** — via `schtasks /Query`.
- **`printScheduledTasks`** — JSON list of the plugin's tasks parsed from
`schtasks /Query /FO CSV`.

iOS-only task types and `cancelByTag` throw `UnsupportedError`.

## Honest limitations

- **Per-user tasks, logged-on only.** Tasks run as the user who registered
them and only while that user is logged on. **"Run whether the user is
logged on or not" requires credentials or SYSTEM and therefore admin
rights** (`schtasks /Create /RU ... /RP ...` or `schtasks /Create /RU
SYSTEM`) — not exposed in the v1 API. An administrator can still edit the
registered task in Task Scheduler and enable it.
- **Minute granularity.** There is no sub-minute scheduling. A zero
`initialDelay` is rounded up to the next minute so one-off tasks still run.
- **Locale-sensitive CLI.** `schtasks` parses dates with the system regional
format; v1 emits `MM/DD/YYYY` and assumes an English (en-US) regional
setting.
- **Constraints and backoff are accepted but ignored** (documented no-ops):
Task Scheduler does not expose `WakeToRun`, battery/AC, idle, network or
storage conditions through the `schtasks` CLI, and there is no WorkManager
style retry/backoff. `existingWorkPolicy` is not honored — re-registering a
`uniqueName` always replaces the task (`/F`).
- No guarantee beyond the registered task itself: the app must be installed
in a way that lets Task Scheduler start it, and the machine must be on when
the task fires.
- `printScheduledTasks` assumes the standard `/Query /FO CSV` columns (Task
Name, Next Run Time, Status).

## Structure

```
workmanager_windows/
lib/
workmanager_windows.dart # WorkmanagerWindows (WorkmanagerPlatform impl)
execution.dart # Flutter-free handler registry + headless runner
src/
process_runner.dart # injectable ProcessRunner (schtasks invocation)
schtasks.dart # schtasks command-line builder (unit-tested)
payload_store.dart # on-disk JSON payload persistence
```

See the [`workmanager_windows` README](../../workmanager_windows/README.md)
for the full API reference.
1 change: 1 addition & 0 deletions melos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ packages:
- workmanager_apple
- workmanager_linux
- workmanager_web
- workmanager_windows
- example
scripts:
get: melos exec -- dart pub get
Expand Down
2 changes: 1 addition & 1 deletion workmanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ This plugin uses a federated architecture with platform-specific implementations
- **workmanager_apple**: iOS implementation using BGTaskScheduler + macOS implementation using NSBackgroundActivityScheduler
- **workmanager_web**: Web implementation using Service Worker + Web Worker (experimental)
- **workmanager_linux**: Linux implementation using systemd user units (experimental)

- **workmanager_windows**: Windows implementation using the Task Scheduler (experimental, see [docs](../docs/windows.mdx))
## 🐛 Support & Issues

- **Documentation**: [docs.page/fluttercommunity/flutter_workmanager](https://docs.page/fluttercommunity/flutter_workmanager)
Expand Down
7 changes: 6 additions & 1 deletion workmanager/lib/src/workmanager_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:workmanager_android/workmanager_android.dart';
import 'package:workmanager_apple/workmanager_apple.dart';
import 'package:workmanager_linux/workmanager_linux.dart';
import 'package:workmanager_web/workmanager_web.dart';
import 'package:workmanager_windows/workmanager_windows.dart';

/// Function that executes your background work.
/// You should return whether the task ran successfully or not.
Expand Down Expand Up @@ -109,13 +110,17 @@ class Workmanager {
return;
}
if (WorkmanagerPlatform.instance is! WorkmanagerAndroid &&
WorkmanagerPlatform.instance is! WorkmanagerApple) {
WorkmanagerPlatform.instance is! WorkmanagerApple &&
WorkmanagerPlatform.instance is! WorkmanagerLinux &&
WorkmanagerPlatform.instance is! WorkmanagerWindows) {
if (Platform.isAndroid) {
WorkmanagerPlatform.instance = WorkmanagerAndroid();
} else if (Platform.isIOS || Platform.isMacOS) {
WorkmanagerPlatform.instance = WorkmanagerApple();
} else if (Platform.isLinux) {
WorkmanagerPlatform.instance = WorkmanagerLinux();
} else if (Platform.isWindows) {
WorkmanagerPlatform.instance = WorkmanagerWindows();
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions workmanager/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dependencies:
workmanager_apple: ^0.9.9
workmanager_web: ^0.1.3
workmanager_linux: ^0.1.1
workmanager_windows:
path: ../workmanager_windows

dev_dependencies:
test: ^1.25.15
Expand Down
5 changes: 5 additions & 0 deletions workmanager_windows/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## 0.0.1

- Experimental Windows implementation of `workmanager` based on Task Scheduler
(`schtasks` CLI): one-off and periodic task registration, cancellation,
status queries, and headless `--background-task` execution.
21 changes: 21 additions & 0 deletions workmanager_windows/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 The Flutter Workmanager Authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
123 changes: 123 additions & 0 deletions workmanager_windows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# workmanager_windows

Windows implementation of `workmanager` for Flutter, backed by **Task
Scheduler**. It registers one-off and periodic tasks as per-user scheduled
tasks via the `schtasks` command line; when a task fires, Windows launches
your app executable with a `--background-task <taskName>` argument, the app
runs the registered callback handler headless and exits.

> ⚠️ **EXPERIMENTAL.** Read [Honest limitations](#honest-limitations) before
> using this package. v1 uses the `schtasks` CLI only — no COM, no native
> code and no `win32` dependency — and is pure Dart.

## How it works

| Situation | Mechanism |
|---|---|
| One-off task | `schtasks /Create /SC ONCE` — runs once at `now + initialDelay` (minute granularity). |
| Periodic task | `schtasks /Create /SC DAILY /RI <minutes>` — repeats every `frequency` (clamped to ≥ 1 minute, ≤ 416 days). |
| Input data | Persisted as a JSON file in `%LOCALAPPDATA%\workmanager_windows\payloads\<uniqueName>.json` and passed to the headless process via `--payload-file`. |
| Execution | Task Scheduler launches `<app.exe> --background-task <taskName> [--payload-file <path>]`; the app runs the registered handler and exits with `0`/`1`. |
| Cancellation | `schtasks /End` (running instance) + `schtasks /Delete` + payload cleanup. |

## Setup

### 1. Add the dependency

```yaml
dependencies:
workmanager_windows: ^0.0.1
```

### 2. Make `main()` headless-aware

```dart
import 'package:workmanager/workmanager.dart';
import 'package:workmanager_windows/workmanager_windows.dart';

@pragma('vm:entry-point')
void callbackDispatcher() {
WorkmanagerWindows().executeTask((taskName, inputData) async {
// Do your background work here. The full Flutter engine is available,
// but no window is shown.
return true;
});
}

void main(List<String> args) {
if (WorkmanagerWindows.maybeRunBackgroundTask(args, callbackDispatcher)) {
return;
}
Workmanager().initialize(callbackDispatcher);
runApp(const MyApp());
}
```

### 3. Register tasks as usual

```dart
final workmanager = Workmanager();
await workmanager.registerOneOffTask(
'unique-name',
'taskName',
inputData: {'key': 'value'},
initialDelay: const Duration(minutes: 5),
);
await workmanager.registerPeriodicTask(
'unique-name',
'taskName',
frequency: const Duration(minutes: 30),
);
```

## API

- `registerOneOffTask(...)` / `registerPeriodicTask(...)` — same signatures as
`Workmanager()`
- `cancelByUniqueName(...)` / `cancelAll()`
- `isScheduledByUniqueName(...)`
- `printScheduledTasks()` — JSON list of the plugin's scheduled tasks
- `maybeRunBackgroundTask(args, callbackDispatcher)` — headless entry point
- `WorkmanagerWindows().executeTask(handler)` — registers the handler inside
the callback dispatcher

iOS-only task types (`registerProcessingTask`, `registerHealthResearchTask`,
`registerContinuedProcessingTask`) and `cancelByTag` throw
`UnsupportedError`.

## Honest limitations

- **Per-user tasks.** Task Scheduler tasks run as the user who registered
them, only while that user is logged on. "Run whether the user is logged on
or not" requires elevated privileges (`schtasks /Create /RU ... /RP ...` or
`SYSTEM`) — not exposed in v1; an admin can enable it for a task manually.
- **Minute granularity.** The scheduler has no sub-minute precision; a zero
`initialDelay` is rounded up to the next minute so one-off tasks still run.
- **Locale-sensitive CLI.** `schtasks` parses dates with the system regional
format; v1 emits `MM/DD/YYYY` and assumes an English (en-US) regional
setting.
- **Constraints/backoff are accepted but ignored** (no-ops): Task Scheduler
does not expose `WakeToRun`, battery/AC, idle or network conditions through
the `schtasks` CLI. Re-registering a `uniqueName` always replaces the task
(`existingWorkPolicy` is not honored).
- No equivalent of WorkManager's guaranteed-while-app-killed semantics beyond
the registered task itself, and no sub-minute or exact-time guarantees.
- `printScheduledTasks` parses `/Query /FO CSV` output and assumes the
standard three columns (TaskName, Next Run Time, Status).

See [`docs/windows.mdx`](../docs/windows.mdx) for the full documentation.

## Structure

```
lib/
workmanager_windows.dart # WorkmanagerWindows (WorkmanagerPlatform impl)
execution.dart # Flutter-free handler registry + headless runner
src/
process_runner.dart # injectable ProcessRunner (schtasks invocation)
schtasks.dart # schtasks command-line builder (unit-tested)
payload_store.dart # on-disk JSON payload persistence
```

The `ProcessRunner` is injectable, so the whole package is unit-testable on
any host without Windows.
4 changes: 4 additions & 0 deletions workmanager_windows/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include: package:lints/recommended.yaml

formatter:
page_width: 120
Loading
Loading