Skip to content

Cancel work

There are two cancel surfaces and they mean different things.

// Unified — cancels EVERYTHING for the TaskId: scheduled work AND in-flight runNow.
val outcome: CancelOutcome = backgrounder.cancel(SyncWorker.ID)

// Narrow — only cancels scheduled work for the TaskId.
val schedulerOutcome: CancelOutcome = backgrounder.scheduler.cancel(SyncWorker.ID)

when (outcome) {
    is CancelOutcome.Cancelled -> log.i { "cleared ${outcome.pendingCleared} pending request(s)" }
    CancelOutcome.NoSuchTask    -> log.i { "no such pending task" }
}
Method Cancels scheduled requests Cancels in-flight scheduled worker Cancels in-flight runNow
Backgrounder.cancel(taskId)
Backgrounder.scheduler.cancel(taskId)
Backgrounder.scheduler.cancelAll() ✓ (all task ids) ✓ (all task ids)

Use Backgrounder.cancel(taskId) unless you specifically need scheduled-only semantics. The pendingCleared count on the returned CancelOutcome.Cancelled reflects the platform-reported scheduled count from the underlying Scheduler.cancel — in-flight runNow cancellations are not added to it (the count's meaning stays consistent with v1).

To cancel everything Backgrounder has scheduled (does not touch in-flight runNow):

backgrounder.scheduler.cancelAll()

cancelAll() only cancels work this library scheduled (Android: matched by the canonical _backgrounder tag; iOS: enumerated from the library's state store). Other WorkManager / BGTaskScheduler work in your app is unaffected.

What "cancel" means per platform

For scheduled work, the OS-imposed primitive decides whether an in-flight worker can be interrupted:

Platform Scheduler.cancel(taskId) interrupts a running worker?
Android YesWorkManager.cancelUniqueWork triggers onStopped, the coroutine job is cancelled.
iOS NoBGTaskScheduler.cancel(taskRequestWithIdentifier:) only kills pending requests. A worker mid-execution finishes whatever it was doing.
macOS YesNSBackgroundActivityScheduler.invalidate() interrupts the running block.

For in-flight runNow, Backgrounder.cancel(taskId) always cancels the lambda on every platform — the deferred completes with CancellationException and the caller's await rethrows. (runNow runs on the calling coroutine context with a platform-specific runway, so the cancellation path is purely in-process; no platform-scheduler involvement.)

The iOS gap on scheduled work is reflected in Scheduler.guarantees().cancelsInFlight = false. If your UX shows a "Cancel" button for scheduled work, branch on this:

val cancelButton = if (scheduler.guarantees().cancelsInFlight) {
    Button("Cancel sync")                     // stops in-flight on Android / macOS
} else {
    Button("Cancel future syncs")             // honest about iOS
}

Cancelling a periodic

For periodic schedules, cancel(taskId) clears the persisted active flag. If a handler is already running on iOS, it sees active = false on completion and skips the resubmit step — so the next interval won't fire. The current run still completes.