Zygote
Builder trackintermediate

I04 — WorkManager sync

Schedule deferrable work with constraints, survive reboots and process death, and read the scheduler's state from the shell. WorkManager is the supported answer to background work, and its guarantees are narrower than most people assume.

3 minAPI 23–37 · 17verified 2026-10-27

WorkManager is the library everyone is told to use and few can describe precisely. This project makes its guarantees concrete: constraints, retries, unique work, and the difference between "enqueued" and "run".

What you are building#

A sync worker that simulates a network call: it waits, fails intermittently, and retries with backoff. The app shows the work's state and lets you constrain it to "network connected" and "charging", so you can watch the scheduler hold the work until the constraints are met.

Steps#

  1. Add WorkManager

    Add the WorkManager runtime and the testing artifact if you want to test without a device. You do not need a separate scheduler library; WorkManager uses JobScheduler internally on modern platforms.1

    You should seeThe dependency resolves.
  2. Write the worker

    Extend CoroutineWorker and implement doWork. Return Result.success(), Result.retry() or Result.failure() based on the simulated outcome.

    You should seeThe worker runs and returns a result.
  3. The worker and the request:

    class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
        override suspend fun doWork(): Result {
            return if (sync()) Result.success() else Result.retry()
        }
    }
     
    val request = OneTimeWorkRequestBuilder<SyncWorker>()
        .setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .setRequiresCharging(true)
                .build()
        )
        .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
        .build()
  4. Add constraints

    Build a Constraints object with a network requirement and a charging requirement, and enqueue the work with them. Toggle airplane mode or unplug the charger and observe the state.

    You should seeThe work waits when the constraint is unmet.
  5. Add backoff

    Return Result.retry() on failure and set a backoff policy and delay in the request. The retry is scheduled, not immediate, and the delay grows.

    You should seeA retry is delayed.
  6. Make it unique

    Use enqueueUniqueWork with ExistingWorkPolicy.KEEP or REPLACE, and observe the difference. Unique work is how you avoid a queue of duplicate syncs.

    You should seeA second enqueue does not create a second chain.
  7. Observe the state

    Collect the work's WorkInfo state and render it. The states are the vocabulary of the system: ENQUEUED, RUNNING, SUCCEEDED, FAILED, CANCELLED, BLOCKED.

    You should seeStates change from ENQUEUED to RUNNING to SUCCEEDED.
  8. Kill the app

    Force-stop the app and observe that the work is cancelled (force-stop cancels scheduled work), then relaunch and see that the persisted work resumes. This is the difference between a process and a scheduled job.2

    You should seeThe work still runs.
  9. Read the scheduler

    Dump the JobScheduler state and find your package. The work is a job with constraints, and the dump shows whether it is ready, waiting or running.

    You should seeA job entry for your work.
Inspect scheduled work
adb shell dumpsys jobscheduler | grep -i -A 5 com.example.sync
adb shell cmd jobscheduler run com.example.sync 1
adb shell dumpsys deviceidle force-idle
Example output
Illustrative output — job ids and package names differ.

Verification#

The work should run when the constraints are met and wait when they are not. A useful test: enqueue with a charging constraint, unplug the device, and confirm the state stays ENQUEUED. Then plug it in and watch it run.

What you learned#

  • WorkManager guarantees eventual execution, not immediate execution. The constraints and the system's batching decide when.
  • Unique work is a queue policy, not a lock. KEEP, REPLACE and APPEND have different semantics and different failure modes.
  • Force-stop cancels scheduled work, which is why a force-stopped app does not sync.
  • The scheduler is visible. dumpsys jobscheduler is the ground truth for what the system thinks is scheduled.

Read Choosing the right background API for the decision tree, and Doze and App Standby for when work is deferred.

Extend it#

  1. Chain two workers with beginWith().then() and observe the ordering and the failure propagation.
  2. Add a periodic worker with a 15-minute minimum interval and watch it run across a device reboot.
  3. Add a foreground worker for a long task and compare the notification and the timeout rules with a foreground service.

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsWorkmanagerdeveloper.android.comaccessed 2026-10-27↩
  2. docsBackground tasks overview | Background workdeveloper.android.comaccessed 2026-10-27↩
  3. docsOptimize for Doze and App Standbydeveloper.android.comaccessed 2026-10-27↩
  4. docsCustom WorkManager configurationdeveloper.android.comaccessed 2026-10-27↩
  5. docsStandby bucketsdeveloper.android.comaccessed 2026-10-27↩
  6. docsForeground services overview | Background workdeveloper.android.comaccessed 2026-10-27↩
  7. docsOptimize power usedeveloper.android.comaccessed 2026-10-27↩