---
title: Task Customization
description: Advanced task configuration with constraints, input data, and management
---

Configure background tasks with constraints, input data, and advanced management options.

## Input Data

Pass data to your background tasks and access it in the callback:

```dart
// Schedule task with input data
Workmanager().registerOneOffTask(
  "upload-task",
  "file_upload",
  inputData: {
    'fileName': 'document.pdf',
    'uploadUrl': 'https://api.example.com/upload',
    'retryCount': 3,
    'userId': 12345,
  },
);

// Access input data in your task
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    print('Task: $task');
    print('Input: $inputData');
    
    // Extract specific values
    String? fileName = inputData?['fileName'];
    String? uploadUrl = inputData?['uploadUrl'];
    int retryCount = inputData?['retryCount'] ?? 0;
    int userId = inputData?['userId'] ?? 0;
    
    // Use the data in your task logic
    await uploadFile(fileName, uploadUrl, userId);
    
    return Future.value(true);
  });
}
```

<Info>
**Input Data Types:** You can pass basic JSON-serializable types: `String`, `int`, `double`, `bool`, `List`, `Map`. Complex objects need to be serialized first.
</Info>

## Task Constraints

Control when tasks should run based on device conditions:

```dart
Workmanager().registerOneOffTask(
  "sync-task",
  "data_sync", 
  constraints: Constraints(
    networkType: NetworkType.connected,      // Require internet connection
    requiresBatteryNotLow: true,            // Don't run when battery is low
    requiresCharging: false,                // Can run when not charging
    requiresDeviceIdle: false,              // Can run when device is active
    requiresStorageNotLow: true,            // Don't run when storage is low
  ),
);
```

### Network Constraints

```dart
// Different network requirements
NetworkType.connected      // Any internet connection
NetworkType.unmetered      // WiFi or unlimited data only  
NetworkType.not_required   // Can run without internet
```

### Battery and Charging

```dart
constraints: Constraints(
  requiresBatteryNotLow: true,    // Wait for adequate battery
  requiresCharging: true,         // Only run when plugged in
)
```

<Warning>
**Platform Differences:** Some constraints are Android-only. iOS background tasks have different system-level constraints that cannot be configured directly, and macOS (`NSBackgroundActivityScheduler`) ignores all constraints.
</Warning>

## Task Management

### Tagging Tasks

Group related tasks with tags for easier management:

```dart
// Tag multiple related tasks
Workmanager().registerOneOffTask(
  "sync-photos",
  "photo_sync",
  tag: "sync-tasks",
);

Workmanager().registerOneOffTask(
  "sync-documents", 
  "document_sync",
  tag: "sync-tasks",
);

// Cancel all tasks with a specific tag
Workmanager().cancelByTag("sync-tasks");
```

### Canceling Tasks

```dart
// Cancel a specific task by unique name
Workmanager().cancelByUniqueName("sync-photos");

// Cancel tasks by tag
Workmanager().cancelByTag("sync-tasks");

// Cancel all scheduled tasks
Workmanager().cancelAll();
```

### Cancelling Running Work (Android)

On Android, `cancelByUniqueName` (and `cancelByTag` / `cancelAll`) stops the
WorkManager worker immediately. The Dart callback that is *currently running*
keeps executing unless it reacts to the stop — register an `onTaskStopped`
handler on `executeTask` inside your `callbackDispatcher` to be notified:

```dart
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask(
    (taskName, inputData) async {
      // The task itself...
      return true;
    },
    onTaskStopped: (taskName, stopReason) async {
      // Persist progress or mark the task as cancelled before the engine
      // shuts down. Return promptly — the platform tears down the task's
      // engine as soon as this handler completes.
      await myDatabase.markCancelled(taskName, stopReason);
    },
  );
}
```

The handler fires whenever WorkManager stops a running worker — not only for
app-initiated cancellation, but also for timeouts, preemption, Doze / App
Standby, and background restrictions. The `stopReason` tells you which case it
was: it mirrors Android's
[`StopReason`](https://developer.android.com/reference/androidx/work/StopReason)
(`cancelledByApp`, `timeout`, `preempt`, ...). On Android versions before 12
(API 31) the reason is always `StopReason.unknown`.

<Info>
**iOS has no equivalent.** `cancelByUniqueName` on iOS only removes *pending*
BGTaskScheduler requests; there is no way to stop a task that is already
running, so `onTaskStopped` is Android-only and never fires on iOS.
</Info>

### Task Scheduling Options

```dart
// One-time task with delay
Workmanager().registerOneOffTask(
  "delayed-task",
  "cleanup",
  initialDelay: Duration(minutes: 30),
  inputData: {'cleanupType': 'cache'}
);

// Periodic task with custom frequency  
Workmanager().registerPeriodicTask(
  "hourly-sync",
  "data_sync",
  frequency: Duration(hours: 1),        // Android: minimum 15 minutes
  initialDelay: Duration(minutes: 5),   // Best-effort hint for the first run (see warning below)
  inputData: {'syncType': 'incremental'}
);
```

<Warning>
**Periodic `initialDelay` is best-effort.** WorkManager (Android) treats it as a
hint: depending on the `androidx.work` version, the first run can happen anywhere
within the first interval. On iOS it becomes the earliest-begin hint for
BGTaskScheduler, which iOS may ignore entirely. If you need the *first* execution
to happen at a specific time, schedule a one-off task with `initialDelay` that
registers the periodic task when it runs.
</Warning>

## iOS: Periodic Timing & Chaining One-off Tasks

On iOS there is no fixed-interval scheduler. `registerPeriodicTask` submits a
`BGAppRefreshTaskRequest` and hands all timing to the system: the `frequency`
you pass from Dart is **ignored on iOS**, and `initialDelay` is used as the
earliest-begin hint (`earliestBeginDate`). iOS then decides *when — and whether
—* the task runs, based on app usage patterns, device state, and battery. 15
minutes is the *minimum gap*, never a cadence: expect runs to be deferred by
hours or skipped entirely.

### Chaining one-off tasks

The Apple-recommended way to approximate periodic work is to schedule the next
run from inside the callback:

```dart
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((taskName, inputData) async {
    // ... do the work ...

    // Schedule the next run. Submitting to BGTaskScheduler (rather than a
    // plain one-off) means the next link survives app relaunches; the plugin
    // re-registers the launch handler from UserDefaults on the next launch.
    Workmanager().registerPeriodicTask(
      "com.example.sync",
      "sync",
      initialDelay: Duration(hours: 1),
    );
    return true;
  });
}
```

For work that should run when the device is idle (and may take minutes), chain
`registerProcessingTask` the same way instead — `BGProcessingTask` gets a
larger budget but only runs while the device is idle, often overnight.

### What chaining buys you — and what it does not

- **"At least N apart", never "every N".** `initialDelay` is a floor, not a
  time. Each link runs whenever iOS chooses, so intervals drift and can
  stretch to hours.
- **The chain advances only when a link runs and re-submits.** If the app is
  force-quit, iOS stops launching it for background work until the user opens
  it again, so a chain that only re-submits from inside the callback stalls.
- **App updates can break the chain.** BGTaskScheduler requests do not
  reliably survive app updates. The plugin's UserDefaults persistence
  re-registers the *launch handlers* on the next launch, but *requests* are
  only re-submitted when your code registers the task again — re-seed the
  chain from your normal startup path (e.g. `main`) if continuity across
  updates matters.
- **Failures reduce future scheduling.** Returning `false` reports the task
  as failed to the system, and repeated failures cause iOS to defer future
  runs. There is no Android-style backoff policy on iOS; implement your own
  by choosing the next `initialDelay`.
- **Energy is metered.** Each opportunistic run costs battery and network.
  Asking for more than the user's usage justifies gets the chain throttled.

### Periodic vs chaining

- **Keep `registerPeriodicTask`** for best-effort content refresh (news,
  sync, cleanup) that tolerates the system pacing it around the user's usage.
- **Chain** when you need a tighter floor between runs, want to vary the gap
  dynamically (e.g. backoff), or need idle-gated heavy work
  (`registerProcessingTask`).
- **Neither gives precision.** Exact-interval background work is not possible
  on iOS. For work that must *start now* on a user action and keep running
  while the app is backgrounded, use `registerContinuedProcessingTask`
  (`BGContinuedProcessingTask`, iOS 26+, shipped in 0.10.0) instead — it is a
  long-running complement, not a scheduling mechanism.

See the [platform capability matrix](index#platform-capability-matrix) for a
side-by-side summary of every task type.

## Advanced Configuration

### Task Identification

Use meaningful, unique task names to avoid conflicts:

```dart
// Good: Specific and unique
Workmanager().registerOneOffTask(
  "user-${userId}-photo-upload-${timestamp}",
  "upload_task",
  inputData: {'userId': userId, 'type': 'photo'}
);

// Avoid: Generic names that might conflict
Workmanager().registerOneOffTask(
  "task1", 
  "upload",
  // ...
);
```

### Task Types and Names

```dart
// Use descriptive task type names in your callback
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    switch (task) {
      case 'photo_upload':
        return await handlePhotoUpload(inputData);
      case 'data_sync':
        return await handleDataSync(inputData);
      case 'cache_cleanup':
        return await handleCacheCleanup(inputData);
      case 'notification_check':
        return await handleNotificationCheck(inputData);
      default:
        print('Unknown task: $task');
        return Future.value(false);
    }
  });
}
```

## Error Handling and Retries

```dart
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    int retryCount = inputData?['retryCount'] ?? 0;
    
    try {
      // Your task logic
      await performTask(inputData);
      return Future.value(true);
      
    } catch (e) {
      print('Task failed: $e');
      
      // Decide whether to retry
      if (retryCount < 3 && isRetryableError(e)) {
        print('Retrying task (attempt ${retryCount + 1})');
        return Future.value(false); // Tell system to retry
      } else {
        print('Task failed permanently');
        return Future.value(true); // Don't retry
      }
    }
  });
}

bool isRetryableError(dynamic error) {
  // Network errors, temporary server issues, etc.
  return error.toString().contains('network') ||
         error.toString().contains('timeout');
}
```

## Best Practices

### Efficient Task Design

```dart
// Good: Quick, focused tasks
@pragma('vm:entry-point') 
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    // Fast operation
    await syncCriticalData();
    return Future.value(true);
  });
}

// Avoid: Long-running operations
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    // This might timeout on iOS (30-second limit)
    await processLargeDataset(); // ❌ Too slow
    return Future.value(true);
  });
}
```

### Resource Management

```dart
@pragma('vm:entry-point')
void callbackDispatcher() {
  Workmanager().executeTask((task, inputData) async {
    HttpClient? client;
    
    try {
      // Initialize resources in the background isolate
      client = HttpClient();
      
      // Perform task
      await performNetworkOperation(client);
      
      return Future.value(true);
      
    } finally {
      // Clean up resources
      client?.close();
    }
  });
}
```

<Success>
**Pro Tip:** Always initialize dependencies inside your background task callback since it runs in a separate isolate from your main app.
</Success>
