Master complex workflows with sequential and parallel task execution.
- What Are Task Chains?
- Sequential Chains
- Parallel Execution
- Mixed Chains
- Real-World Examples
- Best Practices
- Platform Differences
Task chains allow you to execute multiple background tasks in a specific order, with support for both sequential and parallel execution. This is perfect for complex workflows like:
- Download → Process → Upload pipelines
- Parallel data syncing from multiple sources
- Multi-step data transformations
- Batch operations with dependencies
- Automatic dependency management - Tasks run in the correct order
- Error handling - If one task fails, the chain stops
- Parallel execution - Run independent tasks simultaneously
- Type-safe builder API - Fluent, easy-to-read syntax
- Cross-platform - Works on both Android and iOS
Execute tasks one after another, where each task waits for the previous one to complete.
scheduler
.beginWith(TaskRequest(workerClassName = "DownloadWorker"))
.then(TaskRequest(workerClassName = "ProcessWorker"))
.then(TaskRequest(workerClassName = "UploadWorker"))
.enqueue()Execution Order:
DownloadWorker → ProcessWorker → UploadWorker
Pass data between tasks using the input parameter:
scheduler
.beginWith(
TaskRequest(
workerClassName = "FetchUserWorker",
input = "user_id_123"
)
)
.then(
TaskRequest(
workerClassName = "FetchPostsWorker",
input = "user_id_123"
)
)
.then(
TaskRequest(
workerClassName = "CacheDataWorker"
)
)
.enqueue()Apply constraints to specific tasks in the chain:
scheduler
.beginWith(
TaskRequest(
workerClassName = "DownloadWorker",
constraints = Constraints(
requiresNetwork = true,
networkType = NetworkType.UNMETERED // WiFi only
)
)
)
.then(
TaskRequest(
workerClassName = "ProcessWorker",
constraints = Constraints(
requiresCharging = true,
requiresBatteryNotLow = true
)
)
)
.then(
TaskRequest(
workerClassName = "UploadWorker",
constraints = Constraints(
requiresNetwork = true
)
)
)
.enqueue()Execute multiple independent tasks simultaneously, then continue with the next step.
scheduler
.beginWith(listOf(
TaskRequest(workerClassName = "SyncContactsWorker"),
TaskRequest(workerClassName = "SyncCalendarWorker"),
TaskRequest(workerClassName = "SyncPhotosWorker")
))
.then(TaskRequest(workerClassName = "FinalizeWorker"))
.enqueue()Execution Order:
┌─ SyncContactsWorker ─┐
├─ SyncCalendarWorker ─┤ → FinalizeWorker
└─ SyncPhotosWorker ───┘
(run in parallel)
Note: FinalizeWorker only starts after ALL parallel tasks complete successfully.
Each task in a parallel group can have its own constraints:
scheduler
.beginWith(listOf(
TaskRequest(
workerClassName = "DownloadImagesWorker",
constraints = Constraints(
requiresNetwork = true,
networkType = NetworkType.UNMETERED
)
),
TaskRequest(
workerClassName = "DownloadVideosWorker",
constraints = Constraints(
requiresNetwork = true,
networkType = NetworkType.UNMETERED,
requiresCharging = true // Videos need charging
)
),
TaskRequest(
workerClassName = "DownloadDocumentsWorker",
constraints = Constraints(
requiresNetwork = true
)
)
))
.then(TaskRequest(workerClassName = "IndexFilesWorker"))
.enqueue()Combine sequential and parallel execution in complex workflows.
scheduler
.beginWith(listOf(
TaskRequest(workerClassName = "FetchNewsWorker"),
TaskRequest(workerClassName = "FetchWeatherWorker"),
TaskRequest(workerClassName = "FetchStocksWorker")
))
.then(TaskRequest(workerClassName = "MergeDataWorker"))
.then(TaskRequest(workerClassName = "UpdateCacheWorker"))
.then(TaskRequest(workerClassName = "NotifyUserWorker"))
.enqueue()Execution Flow:
┌─ FetchNewsWorker ────┐
├─ FetchWeatherWorker ─┤ → MergeDataWorker → UpdateCacheWorker → NotifyUserWorker
└─ FetchStocksWorker ──┘
scheduler
.beginWith(TaskRequest(workerClassName = "PrepareDataWorker"))
.then(listOf(
TaskRequest(workerClassName = "ProcessImagesWorker"),
TaskRequest(workerClassName = "ProcessVideosWorker"),
TaskRequest(workerClassName = "ProcessAudioWorker")
))
.then(TaskRequest(workerClassName = "CompressWorker"))
.then(TaskRequest(workerClassName = "UploadWorker"))
.enqueue()Execution Flow:
PrepareDataWorker
↓
┌─ ProcessImagesWorker ─┐
├─ ProcessVideosWorker ─┤ → CompressWorker → UploadWorker
└─ ProcessAudioWorker ──┘
scheduler
.beginWith(listOf(
TaskRequest(workerClassName = "DownloadImages1"),
TaskRequest(workerClassName = "DownloadImages2")
))
.then(listOf(
TaskRequest(workerClassName = "ProcessImages1"),
TaskRequest(workerClassName = "ProcessImages2")
))
.then(TaskRequest(workerClassName = "MergeImagesWorker"))
.then(listOf(
TaskRequest(workerClassName = "UploadToServer1"),
TaskRequest(workerClassName = "UploadToServer2"),
TaskRequest(workerClassName = "UploadToBackup")
))
.then(TaskRequest(workerClassName = "CleanupWorker"))
.enqueue()Execution Flow:
┌─ DownloadImages1 ─┐ ┌─ ProcessImages1 ─┐
└─ DownloadImages2 ─┘ → └─ ProcessImages2 ─┘ → MergeImagesWorker
↓
┌─ UploadToServer1 ─┐
├─ UploadToServer2 ─┤ → CleanupWorker
└─ UploadToBackup ──┘
Download model → Train on device → Validate → Upload results
suspend fun updateMLModel() {
scheduler
.beginWith(
TaskRequest(
workerClassName = "DownloadMLModelWorker",
constraints = Constraints(
requiresNetwork = true,
networkType = NetworkType.UNMETERED,
requiresCharging = true
)
)
)
.then(
TaskRequest(
workerClassName = "TrainMLModelWorker",
constraints = Constraints(
isHeavyTask = true, // Long-running task
requiresCharging = true,
requiresBatteryNotLow = true
)
)
)
.then(
TaskRequest(
workerClassName = "ValidateModelWorker",
constraints = Constraints(
requiresCharging = true
)
)
)
.then(
TaskRequest(
workerClassName = "UploadResultsWorker",
constraints = Constraints(
requiresNetwork = true
)
)
)
.enqueue()
}Sync from multiple APIs in parallel, then merge and cache:
suspend fun syncAllData() {
scheduler
.beginWith(listOf(
TaskRequest(
id = "sync-users",
workerClassName = "SyncUsersWorker",
constraints = Constraints(requiresNetwork = true)
),
TaskRequest(
id = "sync-posts",
workerClassName = "SyncPostsWorker",
constraints = Constraints(requiresNetwork = true)
),
TaskRequest(
id = "sync-comments",
workerClassName = "SyncCommentsWorker",
constraints = Constraints(requiresNetwork = true)
),
TaskRequest(
id = "sync-media",
workerClassName = "SyncMediaWorker",
constraints = Constraints(
requiresNetwork = true,
networkType = NetworkType.UNMETERED
)
)
))
.then(
TaskRequest(
workerClassName = "MergeDataWorker"
)
)
.then(
TaskRequest(
workerClassName = "UpdateDatabaseWorker"
)
)
.then(
TaskRequest(
workerClassName = "RefreshUIWorker"
)
)
.enqueue()
}Download → Extract frames → Process frames in parallel → Merge → Upload
suspend fun processVideo(videoId: String) {
scheduler
.beginWith(
TaskRequest(
workerClassName = "DownloadVideoWorker",
input = videoId,
constraints = Constraints(
requiresNetwork = true,
networkType = NetworkType.UNMETERED
)
)
)
.then(
TaskRequest(
workerClassName = "ExtractFramesWorker",
input = videoId
)
)
.then(listOf(
TaskRequest(
workerClassName = "ProcessFrames1Worker",
input = videoId
),
TaskRequest(
workerClassName = "ProcessFrames2Worker",
input = videoId
),
TaskRequest(
workerClassName = "ProcessFrames3Worker",
input = videoId
)
))
.then(
TaskRequest(
workerClassName = "MergeFramesWorker",
input = videoId
)
)
.then(
TaskRequest(
workerClassName = "EncodeVideoWorker",
input = videoId,
constraints = Constraints(
isHeavyTask = true,
requiresCharging = true
)
)
)
.then(
TaskRequest(
workerClassName = "UploadVideoWorker",
input = videoId,
constraints = Constraints(
requiresNetwork = true,
networkType = NetworkType.UNMETERED
)
)
)
.enqueue()
}Backup → Migrate schema → Migrate data in parallel → Verify → Cleanup
suspend fun migrateDatabase() {
scheduler
.beginWith(
TaskRequest(
workerClassName = "BackupDatabaseWorker",
constraints = Constraints(
requiresStorageNotLow = true
)
)
)
.then(
TaskRequest(
workerClassName = "MigrateSchemaWorker"
)
)
.then(listOf(
TaskRequest(workerClassName = "MigrateUsersTableWorker"),
TaskRequest(workerClassName = "MigratePostsTableWorker"),
TaskRequest(workerClassName = "MigrateCommentsTableWorker")
))
.then(
TaskRequest(
workerClassName = "VerifyMigrationWorker"
)
)
.then(
TaskRequest(
workerClassName = "CleanupOldDataWorker"
)
)
.enqueue()
}Each worker should do ONE thing well:
Good:
scheduler
.beginWith(TaskRequest(workerClassName = "DownloadWorker"))
.then(TaskRequest(workerClassName = "ValidateWorker"))
.then(TaskRequest(workerClassName = "ProcessWorker"))
.enqueue()Bad:
// Don't create a mega-worker that does everything
scheduler
.beginWith(TaskRequest(workerClassName = "DownloadValidateProcessWorker"))
.enqueue()Workers should return failure when they can't complete:
class DownloadWorker : IosWorker {
override suspend fun doWork(input: String?): Boolean {
return try {
downloadFile(input)
true // Success
} catch (e: NetworkException) {
Logger.e(LogTags.WORKER, "Download failed", e)
false // Failure - chain will stop
}
}
}On Android:
private suspend fun executeDownloadWorker(input: String?): Result {
return try {
downloadFile(input)
Result.success()
} catch (e: Exception) {
Logger.e(LogTags.WORKER, "Download failed", e)
Result.retry() // WorkManager will retry with backoff
}
}Apply constraints only where needed:
scheduler
.beginWith(
TaskRequest(
workerClassName = "DownloadWorker",
constraints = Constraints(
requiresNetwork = true // Only download needs network
)
)
)
.then(
TaskRequest(
workerClassName = "ProcessWorker"
// No constraints - can run offline
)
)
.then(
TaskRequest(
workerClassName = "UploadWorker",
constraints = Constraints(
requiresNetwork = true // Only upload needs network
)
)
)
.enqueue()Group truly independent tasks in parallel:
Good:
// These tasks don't depend on each other
scheduler.beginWith(listOf(
TaskRequest(workerClassName = "SyncContactsWorker"),
TaskRequest(workerClassName = "SyncCalendarWorker"),
TaskRequest(workerClassName = "SyncPhotosWorker")
))Bad:
// ProcessWorker depends on DownloadWorker - don't parallelize!
scheduler.beginWith(listOf(
TaskRequest(workerClassName = "DownloadWorker"),
TaskRequest(workerClassName = "ProcessWorker") // Will fail!
))Use descriptive task IDs for debugging:
scheduler
.beginWith(
TaskRequest(
id = "ml-pipeline-download",
workerClassName = "DownloadMLModelWorker"
)
)
.then(
TaskRequest(
id = "ml-pipeline-train",
workerClassName = "TrainMLModelWorker"
)
)
.enqueue()Track progress by emitting events from each worker:
class DownloadWorker : IosWorker {
override suspend fun doWork(input: String?): Boolean {
return try {
downloadFile(input)
TaskEventBus.emit(
TaskCompletionEvent(
taskName = "DownloadWorker",
success = true,
message = "✅ Download complete (Step 1/3)"
)
)
true
} catch (e: Exception) {
TaskEventBus.emit(
TaskCompletionEvent(
taskName = "DownloadWorker",
success = false,
message = "❌ Download failed: ${e.message}"
)
)
false
}
}
}Then collect in UI:
@Composable
fun PipelineMonitor() {
var progress by remember { mutableStateOf("Idle") }
LaunchedEffect(Unit) {
TaskEventBus.events.collect { event ->
progress = event.message
}
}
Text("Pipeline: $progress")
}On Android, task chains use WorkManager's continuation API:
// Internally translates to:
WorkManager.getInstance(context)
.beginWith(downloadWork)
.then(processWork)
.then(uploadWork)
.enqueue()Features:
- Full support for sequential and parallel chains
- Automatic retry with backoff
- Persists across device reboots
- Respects Doze mode restrictions
Limitations:
- Minimum 15-minute interval for periodic chains
- Tasks may be delayed by system battery optimizations
On iOS, task chains use a custom queue system with coroutines:
// Internally uses ChainExecutor
val chain = TaskChain(...)
chainQueue.add(chain)
scheduleChainExecutorTask()Features:
- Custom implementation with coroutines
- Supports sequential and parallel execution
- Batch execution for efficiency (up to 3 tasks at once)
- Automatic timeout handling
Limitations:
- BGAppRefreshTask: 25-second timeout per task
- BGProcessingTask: Several minutes (for
isHeavyTask = true) - Tasks only run when app is in background
- iOS decides when to execute (opportunistic)
Best Practices for iOS:
- Keep chains short (max 3-5 tasks)
- Use heavy task mode for long chains:
scheduler .beginWith(TaskRequest( workerClassName = "Step1", constraints = Constraints(isHeavyTask = true) )) .then(TaskRequest(workerClassName = "Step2")) .enqueue() - Break long chains into multiple periodic tasks
- Test on physical devices (simulator behavior differs)
interface TaskChain {
fun then(request: TaskRequest): TaskChain
fun then(requests: List<TaskRequest>): TaskChain
suspend fun enqueue(): ScheduleResult
}data class TaskRequest(
val id: String = UUID.randomUUID().toString(),
val workerClassName: String,
val input: String? = null,
val constraints: Constraints = Constraints()
)interface BackgroundTaskScheduler {
fun beginWith(request: TaskRequest): TaskChain
fun beginWith(requests: List<TaskRequest>): TaskChain
}- API Reference - Complete API documentation
- Constraints & Triggers - All constraint options
- Platform Setup - Platform-specific configuration
- Quick Start - Get started in 5 minutes
Need help? Open an issue or ask in Discussions.