https://github.com/brewkits/kmpworkmanager
KMP WorkManager - Unified API for scheduling and managing background tasks—one‑off, periodic, exact and chained jobs—featuring advanced triggers, structured logging, event‑driven completion, demo UI and docs.
https://github.com/brewkits/kmpworkmanager
alarmmanager backgound-sync background-app-refresh background-fetch background-jobs background-worker bgtaskscheduler compose-multiplatform ios-background kmp kmp-library kotlin kotlin-library kotlin-multiplatform-library kotlin-multiplatform-mobile periodic-background-jobs periodic-tasks scheduled-jobs scheduled-tasks work-manager-kotlin
Last synced: about 1 month ago
JSON representation
KMP WorkManager - Unified API for scheduling and managing background tasks—one‑off, periodic, exact and chained jobs—featuring advanced triggers, structured logging, event‑driven completion, demo UI and docs.
- Host: GitHub
- URL: https://github.com/brewkits/kmpworkmanager
- Owner: brewkits
- License: apache-2.0
- Created: 2026-01-13T07:33:38.000Z (6 months ago)
- Default Branch: main
- Last Pushed: 2026-04-06T02:50:35.000Z (3 months ago)
- Last Synced: 2026-04-06T03:24:25.039Z (3 months ago)
- Topics: alarmmanager, backgound-sync, background-app-refresh, background-fetch, background-jobs, background-worker, bgtaskscheduler, compose-multiplatform, ios-background, kmp, kmp-library, kotlin, kotlin-library, kotlin-multiplatform-library, kotlin-multiplatform-mobile, periodic-background-jobs, periodic-tasks, scheduled-jobs, scheduled-tasks, work-manager-kotlin
- Language: Kotlin
- Homepage: https://www.brewkits.dev
- Size: 9.34 MB
- Stars: 179
- Watchers: 1
- Forks: 4
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: docs/SECURITY.md
Awesome Lists containing this project
README

# KMP WorkManager
[](https://central.sonatype.com/artifact/dev.brewkits/kmpworkmanager)
[](https://kotlinlang.org)
[](https://github.com/brewkits/kmpworkmanager/actions/workflows/build.yml)
[](LICENSE)
**Background task scheduling for Kotlin Multiplatform — including the parts iOS makes hard.**
---
## Installation
```kotlin
// build.gradle.kts
commonMain.dependencies {
implementation("dev.brewkits:kmpworkmanager:2.5.0")
}
```
Android setup
```kotlin
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
KmpWorkManager.initialize(
context = this,
workerFactory = AppWorkerFactory() // Must implement AndroidWorkerFactory
)
}
}
```
iOS setup
**1. AppDelegate**:
```swift
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
override init() {
super.init()
// IOSModuleKt.iosModule calls kmpWorkerModule(workerFactory = IosWorkerFactoryGenerated())
KoinInitializerKt.doInitKoin(platformModule: IOSModuleKt.iosModule)
}
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let koin = KoinIOS()
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "kmp_chain_executor_task",
using: nil
) { task in
IosBackgroundTaskHandler.shared.handleChainExecutorTask(
task: task,
chainExecutor: koin.getChainExecutor()
)
}
return true
}
}
```
**2. `Info.plist`**:
```xml
BGTaskSchedulerPermittedIdentifiers
kmp_chain_executor_task
```
Full setup: [docs/platform-setup.md](docs/platform-setup.md)
---
## Quick start
### Schedule a task
```kotlin
// One-time — runs as soon as constraints are met
scheduler.enqueue(
id = "nightly-sync",
trigger = TaskTrigger.OneTime(initialDelayMs = 0),
workerClassName = "SyncWorker",
constraints = Constraints(requiresNetwork = true)
)
// Periodic — every 15 minutes
scheduler.enqueue(
id = "heartbeat",
trigger = TaskTrigger.Periodic(intervalMs = 15 * 60 * 1000L),
workerClassName = "SyncWorker"
)
```
### Define a worker
You can implement background logic in your `commonMain` code, but KMP WorkManager expects platform-specific factory registration. We recommend using `kmpworker-ksp` to auto-generate this boilerplate.
```kotlin
// commonMain — shared logic
class SyncWorker : Worker {
override suspend fun doWork(input: String?, env: WorkerEnvironment): WorkerResult {
val items = api.fetchPendingItems()
database.upsert(items)
return WorkerResult.Success("Synced ${items.size} items")
}
}
```
```kotlin
// androidMain
import dev.brewkits.kmpworkmanager.annotations.Worker
@Worker(name = "SyncWorker")
class SyncWorkerAndroid : AndroidWorker {
override suspend fun doWork(input: String?, env: WorkerEnvironment) =
SyncWorker().doWork(input, env)
}
// iosMain
import dev.brewkits.kmpworkmanager.annotations.Worker
@Worker(name = "SyncWorker", bgTaskId = "sync_task")
class SyncWorkerIos : IosWorker {
override suspend fun doWork(input: String?, env: WorkerEnvironment) =
SyncWorker().doWork(input, env)
}
```
The `name` argument **must match** the `workerClassName` you pass to `scheduler.enqueue(...)` (`"SyncWorker"` above). Set it explicitly so ProGuard/R8 rename of the wrapper class doesn't break factory lookup.
*Note: Use `AndroidWorkerFactoryGenerated()` and `IosWorkerFactoryGenerated()` in your DI/Initialization if you use KSP. Otherwise, manually implement `AndroidWorkerFactory` and `IosWorkerFactory`.*
### Chain tasks
```kotlin
// Multi-step workflows that survive process death.
// If step 47 of 100 was running when iOS killed the app —
// the next BGTask invocation resumes at step 47, not step 0.
scheduler.beginWith(TaskRequest("DownloadWorker", inputJson = """{"url":"$fileUrl"}"""))
.then(TaskRequest("ValidateWorker"))
.then(TaskRequest("TranscodeWorker"))
.then(TaskRequest("UploadWorker", inputJson = """{"bucket":"processed"}"""))
.withId("transcode-pipeline", policy = ExistingPolicy.KEEP)
.enqueue()
```
---
## Why KMP WorkManager?
Most KMP libraries wrap the happy path — iOS BGTaskScheduler is not just "a different API."
It has a credit system that punishes apps overrunning their time budget, an opaque scheduling policy,
and no recovery mechanism for incomplete work. Getting it wrong means your tasks silently stop running.
| | Android | iOS |
|---|---------|-----|
| Scheduling | Deterministic via WorkManager | Opportunistic — OS decides when |
| Exact timing | ✅ AlarmManager | ⚠️ Best-effort |
| Chain recovery | ✅ WorkContinuation | ✅ Step-level persistence |
| Time budget enforcement | — | ✅ Adaptive (reserves 15–30% safety margin) |
| Queue integrity | ✅ | ✅ CRC32-verified binary format |
| Thread-safe expiry | ✅ | ✅ AtomicInt shutdown flag |
---
## Triggers
| Trigger | Android | iOS | Notes |
|---------|---------|-----|-------|
| `OneTime(delayMs)` | WorkManager | BGTaskScheduler | Minimum delay may be enforced by OS |
| `Periodic(intervalMs)` | WorkManager | BGTaskScheduler | Min 15 min on both platforms |
| `Exact(epochMs)` | AlarmManager | Best-effort | iOS cannot guarantee exact timing |
| `Windowed(earliest, latest)` | WorkManager with delay | BGTaskScheduler | Preferred over Exact on iOS |
| `ContentUri(uri)` | WorkManager ContentUriTrigger | — | Android only |
---
## What's new in v2.5.0
v2.5.0 is a hardening release driven by a production architecture review for camera-app workloads. Highlights:
- **Parallel HTTP download/upload workers** — `ParallelHttpDownloadWorker` splits one
file into N HTTP `Range` chunks (default 4, up to 16) with per-chunk `.partN` resume;
`ParallelHttpUploadWorker` runs one POST per file under a `maxConcurrent` semaphore.
- **Checksum verification** for `HttpDownloadWorker` — `expectedChecksum` +
`ChecksumAlgorithm` (MD5 / SHA-1 / SHA-256 / SHA-512) via Okio's `HashingSource`.
- **DuplicatePolicy** on `HttpDownloadConfig` — `OVERWRITE` (default, preserves
pre-v2.5 behaviour), `SKIP` (return Success without network), `RENAME` (append `_1`,
`_2`, … to the stem).
- **iOS background URLSession download** — `IosBackgroundDownloadWorker` survives
full app termination; persisted state store ensures cold-launch completion events
are delivered to the right `savePath` (the P0 bug fixed in this release).
- **iOS chain retry honoring** — `WorkerResult.Retry(delayMs, attemptCap)` is now
honored at the chain executor level on iOS via `ChainProgress.stepRetryCounts` +
`ChainExecutor.requestedNextBgTaskDelayMs`.
- **Android FGS type configurable** — `KmpHeavyWorker.foregroundServiceType` is
overrideable. Companion-object aliases (`FGS_DATA_SYNC`, `FGS_MEDIA_PROCESSING`,
`FGS_CAMERA`, …) make camera-app workloads first-class.
- **Adversarial test coverage** — collision proof for `PendingIntent` request codes
(CRC32 vs `String.hashCode`), `BroadcastReceiver` lifecycle (Robolectric), iOS
per-step retry counter, backward-compat with v2.4.3-shaped JSON files, cold-launch
survival for background URLSession state.
- **Hard-limit docs** — [`docs/IOS_BGTASK_LIMITS.md`](docs/IOS_BGTASK_LIMITS.md),
[`docs/ANDROID_FGS_GUIDE.md`](docs/ANDROID_FGS_GUIDE.md),
[`docs/APPLE_APP_STORE_REVIEW_GUIDELINES.md`](docs/APPLE_APP_STORE_REVIEW_GUIDELINES.md).
Full breakdown: [`CHANGELOG.md`](CHANGELOG.md). Upgrade notes for users on v2.4.x:
[`docs/MIGRATION_V2.5.0.md`](docs/MIGRATION_V2.5.0.md).
## What's new in v2.4.3
### iOS Dynamic Task IDs (no more Info.plist for every task)
Previously, every task ID had to be declared in `BGTaskSchedulerPermittedIdentifiers` before scheduling. v2.4.3 removes that constraint: only the single master dispatcher ID needs to be in `Info.plist`. All other task IDs are routed through an internal `AppendOnlyQueue` and executed when the OS fires the master dispatcher slot.
```kotlin
// This ID does NOT need to be in Info.plist
scheduler.enqueue(
id = "user-${userId}-daily-sync", // dynamic, per-user ID
trigger = TaskTrigger.Periodic(intervalMs = 24 * 60 * 60 * 1000),
workerClassName = "DailySyncWorker"
)
```
```xml
BGTaskSchedulerPermittedIdentifiers
kmp_master_dispatcher_task
kmp_chain_executor_task
```
### Periodic Task Improvements
Added granular control over the first execution of periodic tasks. You can now defer the initial run or set a specific delay, ensuring your app doesn't choke on heavy sync tasks immediately upon startup.
```kotlin
// Run every 1 hour, but defer the very first run by 1 hour
TaskTrigger.Periodic(
intervalMs = 3600_000,
runImmediately = false
)
```
### Swift Interop 2.0
iOS developers can now use idiomatic `Double` (seconds) instead of `Long` (milliseconds) for all triggers, making the API feel native to the Apple ecosystem.
```swift
// Swift
let trigger = createTaskTriggerPeriodicSeconds(
intervalSeconds: 3600,
initialDelaySeconds: 600
)
```
### iOS Native Background Task Handler
The host application no longer needs to copy and maintain 150+ lines of Swift boilerplate to handle iOS background tasks. The library now provides a native Kotlin API that handles the entire lifecycle:
```swift
// AppDelegate.swift — now just 3 lines to handle any task
BGTaskScheduler.shared.register(forTaskWithIdentifier: taskId, using: nil) { task in
IosBackgroundTaskHandler.shared.handleSingleTask(
task: task,
scheduler: koin.getScheduler(),
executor: koin.getExecutor()
)
}
```
This handler automatically:
- Sets up the `expirationHandler` for graceful shutdown.
- Resolves worker metadata (`workerClassName`, `inputJson`) from file storage.
- Executes the worker with timeout protection via `SingleTaskExecutor`.
- **Auto-reschedules periodic tasks** and the chain executor if the queue is not empty.
- Performs deadline checks for windowed tasks.
### Hardened iOS Persistence & Safety
We've overhauled the iOS storage engine to ensure industrial-grade reliability:
- **Okio Streaming**: All file operations (Events, History, Queue) now use Okio. Peak RAM usage is now constant (O(1)) regardless of file size, preventing OOM kills on older devices.
- **Race Condition Fixes**: Critical fix in `IosFileCoordinator` using `AtomicInt` to ensure background blocks are executed exactly once, even during high-concurrency stress or timeouts.
- **Self-Healing Queue**: We now detect CRC32 checksum mismatches and automatically recover/reset corrupted records to prevent persistent job stalls.
- **UTF-8 Safety**: Guaranteed safety against multi-byte character corruption (Emoji/CJK) at chunk boundaries.
### Execution history (v2.3.8)
Every chain execution is persisted locally. Collect, upload, clear:
```kotlin
lifecycleScope.launch {
val records = scheduler.getExecutionHistory(limit = 200)
if (records.isNotEmpty()) {
analyticsService.uploadBatch(records)
scheduler.clearExecutionHistory()
}
}
```
Each `ExecutionRecord` carries `chainId`, `status` (SUCCESS / FAILURE / ABANDONED / SKIPPED / TIMEOUT), `durationMs`, step counts, error message, retry count, and platform. Up to 500 records kept; older ones pruned automatically.
### Telemetry hook
Route task lifecycle events to Sentry, Crashlytics, or Datadog:
```kotlin
KmpWorkManagerConfig(
telemetryHook = object : TelemetryHook {
override fun onTaskFailed(event: TelemetryHook.TaskFailedEvent) {
Sentry.captureMessage("Task failed: ${event.taskName} — ${event.error}")
}
override fun onChainFailed(event: TelemetryHook.ChainFailedEvent) {
analytics.track("chain_failed", mapOf(
"chainId" to event.chainId,
"failedStep" to event.failedStep
))
}
}
)
```
Six events: `onTaskStarted`, `onTaskCompleted`, `onTaskFailed`, `onChainCompleted`, `onChainFailed`, `onChainSkipped`. All have default no-op implementations.
### Task priority
`LOW`, `NORMAL`, `HIGH`, `CRITICAL`. On Android, `HIGH`/`CRITICAL` map to expedited work. On iOS, the queue is sorted by priority before each BGTask window:
```kotlin
scheduler.beginWith(
TaskRequest(workerClassName = "PaymentSyncWorker", priority = TaskPriority.CRITICAL)
).enqueue()
```
### Battery guard
```kotlin
KmpWorkManagerConfig(minBatteryLevelPercent = 10) // defer when < 10% and not charging
```
Default `5%`. Works on both platforms.
### KSP: BGTask ID validation
```kotlin
// iosMain
@Worker("SyncWorker", bgTaskId = "com.example.sync-task")
class SyncWorker : IosWorker { ... }
// kmpWorkerModule() automatically validates bgTaskId against Info.plist at startup
kmpWorkerModule(workerFactory = IosWorkerFactoryGenerated())
```
Add to `build.gradle.kts`:
```kotlin
plugins { id("com.google.devtools.ksp") }
dependencies {
ksp("dev.brewkits:kmpworker-ksp:2.5.0")
commonMain.implementation("dev.brewkits:kmpworker-annotations:2.5.0")
}
```
---
## Built-in workers
| Worker | Status | Notes |
|--------|--------|-------|
| `HttpRequestWorker` | Stable | One-shot HTTP with configurable method, headers, body. SSRF-validated. |
| `HttpDownloadWorker` | Stable (v2.5+) | Resumable download via HTTP `Range`. `.partial` survives process kill; a process kill resumes from last byte. Supports SHA-256/SHA-1/SHA-512/MD5 checksum verification and `DuplicatePolicy` (overwrite / skip / rename). |
| `ParallelHttpDownloadWorker` | New in v2.5 | Splits a single file into N (1..16, default 4) HTTP `Range` chunks downloaded concurrently with per-chunk `.partN` resume. Automatic sequential fallback when the server does not advertise `Accept-Ranges: bytes`. Same checksum verification surface as `HttpDownloadWorker`. |
| `HttpUploadWorker` | ⚠️ Experimental | Streaming multipart upload. No resumable / chunked upload yet (see `ParallelHttpUploadWorker` for multi-file uploads). |
| `ParallelHttpUploadWorker` | New in v2.5 | One POST per file with per-host `maxConcurrent` limit (1..16, default 3) and per-file retry on 5xx / network errors (`maxRetries` 0..5). Per-file outcomes exposed via `WorkerResult.Success.data.fileResults`. |
| `IosBackgroundDownloadWorker` | iOS-only, experimental (v2.5+) | Hands the download to `URLSessionConfiguration.background` so the transfer survives **full app termination**. Host AppDelegate must wire `application(_:handleEventsForBackgroundURLSession:completionHandler:)` — see [docs/IOS_BACKGROUND_URL_SESSION.md](docs/IOS_BACKGROUND_URL_SESSION.md). |
| `HttpSyncWorker` | Stable | Fetch-and-persist data sync. |
| `FileCompressionWorker` | ✅ Android · 🚧 iOS | **iOS has no ZIP codec in Kotlin/Native.** The default behavior on iOS is to **fail fast** with an explicit error. Set `FileCompressionConfig.allowIosUncompressedFallback = true` to accept an uncompressed copy at the output path (useful for demo chains; the output is **not** a real ZIP). For real iOS compression, integrate [ZIPFoundation](https://github.com/weichsel/ZIPFoundation) via cinterop. |
> **Camera / media-app advisory.** For burst upload (50 photos at once), use
> `ParallelHttpUploadWorker` instead of one chain step per file. For RAW / video
> downloads over cellular, prefer `IosBackgroundDownloadWorker` on iOS so the
> transfer survives swipe-to-quit. `HttpUploadWorker` is the only stable worker
> without resumable/chunked semantics — pin those uploads to Wi-Fi
> (`Constraints(requiresUnmeteredNetwork = true)`) until v2.6.
---
## Security
**SSRF protection** — all built-in worker HTTP calls are validated before dispatch. Blocked:
```
169.254.169.254 AWS/GCP/Azure IMDS
fd00:ec2::254 AWS EC2 (IPv6)
100.100.100.200 Alibaba Cloud metadata
localhost, 0.0.0.0/8, [::1], 10.x, 172.16–31.x, 192.168.x
100.64.0.0/10 CGNAT (Tailscale, carrier-grade NAT)
fc00::/7, fe80::/10
```
RFC 3986 UserInfo bypass and multi-`@` authority attacks are both handled. DNS rebinding is a known limitation — use certificate pinning or an egress proxy for high-trust environments.
**Input size validation** — inputs exceeding WorkManager's 10 KB `Data` limit throw `IllegalArgumentException` at enqueue time.
---
## Testing
```
600+ tests across commonTest, iosTest, androidInstrumentedTest
```
- `QA_PersistenceResilienceTest` — 100-step chain killed at step 50, resumes at exactly step 50
- `V236ChainExecutorTest` — time budget, shutdown propagation, batch loop correctness
- `IosExecutionHistoryStoreTest` — save/get/clear, auto-pruning, all status variants
- `AppendOnlyQueueTest` — CRC32 corruption detection, truncation recovery, concurrent access
- `SecurityValidatorTest` — SSRF, IPv6 compressed loopback, multi-`@` UserInfo bypass
---
## Documentation
| | |
|---|---|
| [Quick Start](docs/quickstart.md) | Running in 5 minutes |
| [Platform Setup](docs/platform-setup.md) | Android & iOS configuration |
| [API Reference](docs/api-reference.md) | Full public API |
| [Task Chains](docs/task-chains.md) | Chain API and recovery semantics |
| [Built-in Workers](docs/BUILTIN_WORKERS_GUIDE.md) | Worker reference and input schema |
| [Constraints & Triggers](docs/constraints-triggers.md) | All scheduling options |
| [iOS Best Practices](docs/ios-best-practices.md) | BGTask gotchas and recommendations |
| [iOS BGTask Hard Limits](docs/IOS_BGTASK_LIMITS.md) | Opportunistic scheduling, time budget, headless DI |
| [App Store Review Compliance](docs/APPLE_APP_STORE_REVIEW_GUIDELINES.md) | §2.5.4 — what gets rejected and how to ship safely |
| [Android FGS Type Guide](docs/ANDROID_FGS_GUIDE.md) | `mediaProcessing` / `camera` / `dataSync` setup |
| [iOS Background URLSession](docs/IOS_BACKGROUND_URL_SESSION.md) | Surviving app termination during long downloads |
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common issues |
| [CHANGELOG](CHANGELOG.md) | Release history |
**Migration:** [v2.2.2 → v2.3.0](docs/MIGRATION_V2.3.0.md) · [v2.3.3 → v2.3.4](docs/MIGRATION_V2.3.3_TO_V2.3.4.md) · [v2.4.x → v2.5.0](docs/MIGRATION_V2.5.0.md)
---
## Requirements
| | |
|---|---|
| Kotlin | 2.1.0+ |
| Android | 8.0+ (API 26) |
| iOS | 13.0+ |
| Gradle | 8.0+ |
---
## Contributing
```bash
./gradlew :kmpworker:allTests # all platforms must pass before opening a PR
```
Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/).
---
## License
Apache 2.0. See [LICENSE](LICENSE).
---
[GitHub](https://github.com/brewkits/kmpworkmanager) · [Maven Central](https://central.sonatype.com/artifact/dev.brewkits/kmpworkmanager) · [Issues](https://github.com/brewkits/kmpworkmanager/issues)