Background Tasks on Linux: workmanager_linux Runs Your Dart Callbacks as systemd Units
workmanager's Linux plugin schedules Flutter background work with real systemd timers — how it works and the constraints to design around.
Published on • October 20, 2026
AI Assistant

If you ship a Flutter desktop app to Linux, you’ve probably hit the gap: Android has WorkManager, iOS has BGTaskScheduler, and Linux had nothing — just cron if you were lucky. That gap is what workmanager_linux (0.1.1+1, published August 3, 2026) fills. It’s the experimental Linux implementation of the workmanager plugin, and instead of approximating a scheduler inside your process, it leans on systemd user units: real .timer/.service pairs and transient units created through systemd-run.
The federated plugin you already depend on
workmanager 0.10.10 (fluttercommunity.dev, published September 7, 2026; 2.4k likes and 322k downloads shown on pub.dev) is a federated plugin. Your pubspec.yaml resolves one API package and several platform implementations:
- workmanager — the unified Dart API
- workmanager_android — Android WorkManager
- workmanager_apple — iOS BGTaskScheduler + macOS NSBackgroundActivityScheduler
- workmanager_web — experimental Service Worker / Web Worker execution
- workmanager_linux — experimental systemd user units
- workmanager_platform_interface — the contract between them
Because workmanager depends on workmanager_linux: ^0.1.1 directly, Linux support arrives with a normal dependency bump — you keep calling Workmanager() exactly as before:
dependencies:
workmanager: ^0.10.0
workmanager_linux: ^0.1.1
How scheduling actually works under systemd
Unlike the web implementation, Linux has a genuine OS scheduler, so the plugin contract — “execute Dart code in the background, even when your app is closed” — is implemented with systemd itself:
| Task type | Mechanism |
|---|---|
| One-off | Transient units via systemd-run --user --unit=workmanager-<hash> --on-active=<delay> (or --no-block for immediate runs). The unit vanishes after the run. |
| Periodic | A .timer/.service unit pair written to ~/.config/systemd/user/. The timer uses OnUnitActiveSec for its interval and Persistent=true for WorkManager-style catch-up of runs missed while the machine was off. |
OnUnitActiveSec is the systemd directive that fires a timer a fixed interval after the service last ran — that’s your frequency: Duration(hours: 1). Persistent=true is the equivalent of Android’s catch-up behavior: if the laptop was powered off when a run was due, the service triggers on next boot.
When a timer fires, the service launches your app binary in headless mode:
your_app --background-task <taskName> --payload <path>
The payload is the inputData JSON you passed at registration, persisted Android-style under $XDG_DATA_HOME/workmanager/payloads/. There is no native code and no Pigeon here — the package is pure Dart that shells out to systemctl --user / systemd-run --user through an injectable process runner, which is why its test suite asserts the exact commands and unit file contents without needing systemd installed.
Wiring up the headless entrypoint
Your main() must detect the --background-task invocation and run the callback instead of starting the UI, and the headless process has no native platform-channel counterpart — so the dispatcher registers with WorkmanagerLinux.executeTask, not Workmanager().executeTask:
import 'package:flutter/material.dart';
import 'package:workmanager/workmanager.dart';
import 'package:workmanager_linux/workmanager_linux.dart';
Future<void> main(List<String> args) async {
if (await WorkmanagerLinux.maybeRunBackgroundTask(args, callbackDispatcher)) {
// Launched headless by systemd; the result was logged and the process exited
// (exit code 0 = success, 1 = failure).
return;
}
runApp(const MyApp());
}
@pragma('vm:entry-point')
void callbackDispatcher() {
WorkmanagerLinux.executeTask((taskName, inputData) async {
print('Background task: $taskName');
return true;
});
}
Scheduling stays on the familiar API:
Workmanager().initialize(callbackDispatcher);
Workmanager().registerOneOffTask('task-id', 'sync', initialDelay: const Duration(minutes: 5));
Workmanager().registerPeriodicTask('periodic-id', 'sync', frequency: const Duration(hours: 1));
The dispatcher’s exit code is the task result, so failed runs show up as failed units in journalctl --user — a genuinely nicer debugging story than mobile background execution.
Constraints you must design around
The package README is unusually blunt about what v1 doesn’t do. Read these before committing:
- Constraints are accepted but ignored.
networkType,requiresCharging, and friends parse and then do nothing — no battery, AC, or network gating. - Backoff policy is accepted but ignored. Failed one-off tasks are not retried; a failed periodic task just waits for the next interval.
existingWorkPolicyis effectivelyREPLACE. Re-registering a unique name overwrites the units;KEEPisn’t implemented.- Tags are accepted but not tracked.
cancelByTagthrowsUnsupportedError— cancel by unique name or usecancelAll(). - No Android-style 15-minute floor. Frequency is honored as-is (systemd resolves
OnUnitActiveSecto whole seconds, so sub-minute intervals aren’t useful). - iOS-only task types throw.
registerProcessingTask,registerHealthResearchTask, andregisterContinuedProcessingTaskare unsupported. - User timers stop at logout unless you run
loginctl enable-linger $USER, and on many desktops user services only start after the first graphical login. - Flatpak and Snap sandboxes can’t write user units, so packaged apps in those formats are out of scope for now.
Requirements and debugging
You need a systemd-based distro with a working user session; from a normal desktop session it just works. When things go wrong, the usual systemd tooling applies:
systemctl --user list-timers # is the timer armed, when does it next fire?
journalctl --user -u 'your_app*' # task output and failed units
systemctl --user cat workmanager-* # inspect the generated unit files
If you’re evaluating background sync for a Flutter Linux app today, the honest summary is: one-off and periodic tasks with name-based cancellation are solid, everything else (constraints, backoff, tags, sandboxed packaging) is a known gap — track the DESIGN.md and the repo’s issues for v1 progress.