Workmanager provides detailed task status tracking through debug handlers, allowing you to monitor the complete lifecycle of your background tasks from scheduling to completion.
Background tasks go through several status states during their lifecycle. The Workmanager plugin tracks these states and provides notifications through debug handlers.
| Status | Description | When it occurs | Android | iOS |
|---|---|---|---|---|
| Scheduled | Task has been scheduled with the system | When registerOneOffTask() or registerPeriodicTask() is called | ✅ | ✅ |
| Started | Task execution has begun (first attempt) | When task starts running for the first time | ✅ | ✅ |
| Retrying | Task is being retried after a previous attempt | When task starts running after runAttemptCount > 0 | ✅ | ❌ |
| Rescheduled | Task will be retried later | When Dart function returns false | ✅ | ❌ |
| Completed | Task finished successfully | When Dart function returns true | ✅ | ✅ |
| Failed | Task failed permanently | When Dart function throws an exception | ✅ | ✅ |
| Cancelled | Task was cancelled before completion | When cancelAll() or cancelByUniqueName() is called | ✅ | ✅ |
The behavior of task status depends on what your Dart background function returns:
| Dart Return | Task Status | System Behavior | Debug Notification |
|---|---|---|---|
true | Completed | Task succeeds, won't retry | ✅ Success |
false | Rescheduled | WorkManager schedules retry with backoff | 🔄 Rescheduled |
Future.error() | Failed | Task fails permanently, no retry | ❌ Failed + error |
| Exception thrown | Failed | Task fails permanently, no retry | ❌ Failed + error |
On Android, Workmanager can distinguish between first attempts and retries using runAttemptCount:
| Scenario | Start Status | End Status | Notification Example |
|---|---|---|---|
| Fresh task, succeeds | Started | Completed | ▶️ Started → ✅ Success |
| Fresh task, returns false | Started | Rescheduled | ▶️ Started → 🔄 Rescheduled |
| Retry attempt, succeeds | Retrying | Completed | 🔄 Retrying → ✅ Success |
| Retry attempt, fails | Retrying | Failed | 🔄 Retrying → ❌ Failed |
When a task returns false, Android WorkManager uses exponential backoff by default:
- 1st retry: ~30 seconds
- 2nd retry: ~1 minute
- 3rd retry: ~2 minutes
- Maximum: ~5 hours
Task status is exposed through debug handlers. Set up debug handlers to receive status notifications:
The NotificationDebugHandler supports custom notification channels and grouping:
Android Options:
channelId: Custom notification channel ID for organizing notifications (if custom, you must create the channel first)channelName: Human-readable channel name shown in system settings (only used if using default channel)groupKey: Groups related notifications together in the notification drawer
iOS Options:
categoryIdentifier: Custom notification category for specialized handlingthreadIdentifier: Groups notifications in the same conversation thread
// NotificationDebugHandler - shows status as notifications
WorkmanagerDebug.setCurrent(NotificationDebugHandler())
// Custom notification channel and grouping (you must create the channel first)
val channelId = "MyAppDebugChannel"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(channelId, "My App Debug", NotificationManager.IMPORTANCE_DEFAULT)
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
WorkmanagerDebug.setCurrent(NotificationDebugHandler(
channelId = channelId,
groupKey = "workmanager_debug_group"
))
// LoggingDebugHandler - writes to system log
WorkmanagerDebug.setCurrent(LoggingDebugHandler())
// Custom handler
class CustomDebugHandler : WorkmanagerDebug() {
override fun onTaskStatusUpdate(
context: Context,
taskInfo: TaskDebugInfo,
status: TaskStatus,
result: TaskResult?
) {
when (status) {
TaskStatus.SCHEDULED -> log("Task scheduled: ${taskInfo.taskName}")
TaskStatus.STARTED -> log("Task started: ${taskInfo.taskName}")
TaskStatus.RETRYING -> log("Task retrying (attempt ${taskInfo.runAttemptCount}): ${taskInfo.taskName}")
TaskStatus.RESCHEDULED -> log("Task rescheduled: ${taskInfo.taskName}")
TaskStatus.COMPLETED -> log("Task completed: ${taskInfo.taskName}")
TaskStatus.FAILED -> log("Task failed: ${taskInfo.taskName}, error: ${result?.error}")
TaskStatus.CANCELLED -> log("Task cancelled: ${taskInfo.taskName}")
}
}
}The built-in NotificationDebugHandler shows concise, actionable notifications:
| Status | Title Format | Body |
|---|---|---|
| Scheduled | 📅 Scheduled | taskName |
| Started | ▶️ Started | taskName |
| Retrying | 🔄 Retrying | taskName |
| Rescheduled | 🔄 Rescheduled | taskName |
| Success | ✅ Success | taskName |
| Failed | ❌ Failed | taskName + error message |
| Exception | ❌ Exception | taskName + exception details |
| Cancelled | ⏹️ Cancelled | taskName |
- Retry detection: Can distinguish first attempts from retries
- Automatic rescheduling: WorkManager handles retry logic with backoff
- Rich debug info: Access to
runAttemptCountand system constraints - Guaranteed execution: Tasks will retry according to policy
- No retry detection: Cannot distinguish first attempts from retries
- Manual rescheduling: App must reschedule tasks on failure
- System controlled: iOS decides when/if tasks actually run
- No guarantees: Tasks may never execute depending on system state
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
try {
// Your task logic here
final result = await performWork(task, inputData);
if (result.isSuccess) {
return true; // ✅ Task succeeded
} else {
return false; // 🔄 Retry with backoff (Android) or manual reschedule (iOS)
}
} catch (e) {
// 🔥 Permanent failure - will not retry
throw Exception('Task failed: $e');
}
});
}| Error Type | Recommended Return | Result |
|---|---|---|
| Network timeout | return false | Task will retry later |
| Invalid data | throw Exception() | Task fails permanently |
| Temporary server error | return false | Task will retry with backoff |
| Authentication failure | throw Exception() | Task fails, needs user intervention |
// Track task execution in your debug handler
class TaskHealthMonitor : WorkmanagerDebug() {
override fun onTaskStatusUpdate(context: Context, taskInfo: TaskDebugInfo, status: TaskStatus, result: TaskResult?) {
when (status) {
TaskStatus.COMPLETED -> recordSuccess(taskInfo.taskName)
TaskStatus.FAILED -> recordFailure(taskInfo.taskName, result?.error)
TaskStatus.RETRYING -> recordRetry(taskInfo.taskName)
}
}
}Tasks showing as "Rescheduled" but not running:
- Android: Check battery optimization and Doze mode settings
- iOS: Verify Background App Refresh is enabled and app is used regularly
Tasks immediately failing:
- Check if task logic throws exceptions during initialization
- Verify all dependencies are available in background isolate
- Review error messages in Failed notifications
No status notifications appearing:
- Ensure debug handler is set before task execution
- Check notification permissions (for NotificationDebugHandler)
- Verify debug handler is called during task lifecycle
For detailed debugging guidance, see the Debugging Guide.
If you were using the deprecated isInDebugMode parameter:
// ❌ Old approach (deprecated)
await Workmanager().initialize(
callbackDispatcher,
isInDebugMode: true, // Deprecated
);
// ✅ New approach
await Workmanager().initialize(callbackDispatcher);
// Set up platform-specific debug handler
// Android: WorkmanagerDebug.setCurrent(NotificationDebugHandler())
// iOS: WorkmanagerDebug.setCurrent(LoggingDebugHandler())The new system provides much more detailed and customizable debugging information than the simple boolean flag.