src/skills/mobile-background-tasks/SKILL.md
Background fetch, processing tasks, background location, headless JS, battery optimization - Expo and bare React Native
npx skillsauth add agents-inc/skills mobile-background-tasksInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: Background tasks in React Native are heavily constrained by OS power management. Use
expo-background-task(Expo) orreact-native-background-fetch(bare RN) for periodic fetch. Useexpo-locationfor background location tracking. iOS gives ~30s for refresh tasks (BGAppRefreshTask) and several minutes for processing tasks (BGProcessingTask). Android enforces 15-minute minimum intervals via WorkManager and restricts execution in Doze mode. Always callfinish()or return a result when done -- the OS will terminate tasks that exceed their time budget.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST define tasks in the top-level scope (global) -- tasks defined inside React components or lifecycle methods will NOT be registered when the app starts from the background)
(You MUST call finish(taskId) or return a BackgroundTaskResult when task execution completes -- failing to signal completion causes the OS to penalize or kill your app)
(You MUST request background permissions explicitly on both platforms -- iOS requires Info.plist UIBackgroundModes entries, Android requires manifest permissions)
(You MUST handle the OS killing your task at any time -- use expiration listeners on iOS and timeout callbacks on Android to clean up gracefully)
(You MUST keep background work minimal -- sync only changed data, avoid heavy computation, respect the ~30s iOS refresh limit)
</critical_requirements>
Auto-detection: expo-task-manager, expo-background-task, expo-background-fetch, expo-location background, react-native-background-fetch, BackgroundFetch, TaskManager, defineTask, registerTaskAsync, startLocationUpdatesAsync, Headless JS, registerHeadlessTask, BGTaskScheduler, WorkManager, background fetch, background processing, background location
When to use:
When NOT to use:
Key patterns covered:
expo-background-task (new) and expo-background-fetch (legacy)react-native-background-fetch with configure/scheduleTaskexpo-location and TaskManagerDetailed Resources:
Background execution on mobile is a privilege, not a right. Both iOS and Android aggressively limit what apps can do in the background to preserve battery life and user experience. The OS decides when (and whether) your task runs -- you can only request execution and set minimum intervals.
Core principles:
finish(). The OS tracks your task duration and penalizes apps that don't complete promptly.The background execution spectrum:
Most reliable Least reliable
| |
Push notifications > Foreground services > Background tasks > Timers
(instant delivery) (visible to user) (OS-scheduled) (killed)
Background tasks sit in the middle -- more reliable than timers, but entirely at the OS's discretion. For critical work, combine with push notifications as a trigger.
</philosophy>The modern Expo approach using BGTaskScheduler (iOS) and WorkManager (Android). Replaces the older expo-background-fetch.
import * as TaskManager from "expo-task-manager";
import * as BackgroundTask from "expo-background-task";
const SYNC_TASK_NAME = "BACKGROUND_SYNC_TASK";
const TWELVE_HOURS_IN_MINUTES = 720;
// MUST be top-level -- not inside a component
TaskManager.defineTask(SYNC_TASK_NAME, async () => {
try {
const hasNewData = await fetchLatestUpdates();
return hasNewData
? BackgroundTask.BackgroundTaskResult.Success
: BackgroundTask.BackgroundTaskResult.Failed;
} catch {
return BackgroundTask.BackgroundTaskResult.Failed;
}
});
Why good: Task defined at top-level scope (runs even when app launches in background), returns explicit result code, handles errors
// BAD: Defining task inside a component
function App() {
useEffect(() => {
// This will NOT work when app starts from background
TaskManager.defineTask(SYNC_TASK_NAME, async () => {
/* ... */
});
}, []);
}
Why bad: Task definition inside component lifecycle will not execute when the OS launches the app headlessly in the background
See examples/core.md for complete registration/unregistration lifecycle.
For bare React Native projects (non-Expo). Wraps BGAppRefreshTask (iOS) and WorkManager (Android).
import BackgroundFetch from "react-native-background-fetch";
const MIN_FETCH_INTERVAL_MINUTES = 15;
// Configure in app initialization (e.g., App component mount)
const status = await BackgroundFetch.configure(
{
minimumFetchInterval: MIN_FETCH_INTERVAL_MINUTES,
stopOnTerminate: false, // Android: continue after app killed
startOnBoot: true, // Android: restart after device reboot
enableHeadless: true, // Android: enable Headless JS
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY,
},
async (taskId) => {
// Task triggered -- do your work
await syncData();
BackgroundFetch.finish(taskId); // MUST call when done
},
async (taskId) => {
// Timeout -- OS is about to kill the task, clean up immediately
BackgroundFetch.finish(taskId);
},
);
Why good: Explicit timeout handler for graceful cleanup, finish(taskId) signals OS completion, Android-specific options for post-termination behavior
See examples/core.md for scheduleTask one-shot/periodic tasks and Headless JS setup.
Continuous location updates while backgrounded. Uses expo-location with expo-task-manager. Requires explicit background permission grants ("Always Allow" on iOS).
import * as TaskManager from "expo-task-manager";
import * as Location from "expo-location";
const LOCATION_TASK_NAME = "BACKGROUND_LOCATION_TASK";
// Top-level task definition
TaskManager.defineTask(LOCATION_TASK_NAME, async ({ data, error }) => {
if (error) {
console.error("Location task error:", error.message);
return;
}
if (data) {
const { locations } = data as { locations: Location.LocationObject[] };
await saveLocationsToServer(locations);
}
});
Why good: Top-level definition, explicit error handling, typed location data extraction
See examples/core.md for permission flow, start/stop, and accuracy configuration.
Android-only mechanism for running JavaScript after app termination. Requires native setup and registration in index.js.
// index.js -- register headless task alongside app
import { AppRegistry } from "react-native";
import { App } from "./App";
const APP_NAME = "MyApp";
const HEADLESS_TASK_NAME = "com.transistorsoft.fetch"; // Default ID from react-native-background-fetch
AppRegistry.registerComponent(APP_NAME, () => App);
// Headless task runs when app is terminated but task fires
AppRegistry.registerHeadlessTask(HEADLESS_TASK_NAME, () => async (taskData) => {
await performSync(taskData);
// Task completes when promise resolves
});
Why good: Registered at app entry point, async function allows proper cleanup, runs even after app termination on Android
Gotcha: Headless JS is Android-only. iOS has no equivalent -- once the user force-quits the app, no background tasks run until the user reopens it.
See examples/core.md for complete headless setup with enableHeadless configuration.
Always check registration status before registering, and unregister when tasks are no longer needed.
async function ensureBackgroundSyncRegistered(): Promise<void> {
const isRegistered = await TaskManager.isTaskRegisteredAsync(SYNC_TASK_NAME);
if (isRegistered) return;
await BackgroundTask.registerTaskAsync(SYNC_TASK_NAME, {
minimumInterval: TWELVE_HOURS_IN_MINUTES,
});
}
async function disableBackgroundSync(): Promise<void> {
const isRegistered = await TaskManager.isTaskRegisteredAsync(SYNC_TASK_NAME);
if (!isRegistered) return;
await BackgroundTask.unregisterTaskAsync(SYNC_TASK_NAME);
}
Why good: Guards against double-registration, idempotent enable/disable, named constants for intervals
See examples/core.md for status checking and debugging patterns.
</patterns><decision_framework>
What kind of background work do you need?
|
+-> Periodic data sync (every 15min - 12hrs)?
| +-> Expo project? --> expo-background-task
| +-> Bare RN? --> react-native-background-fetch
|
+-> Continuous location tracking?
| +-> Expo? --> expo-location + startLocationUpdatesAsync
| +-> Bare RN? --> react-native-background-geolocation
|
+-> Complete a task started in foreground (iOS 26+)?
| +-> BGContinuedProcessingTask (new in iOS 26)
|
+-> Long-running processing (ML, export)?
| +-> Foreground service with notification (Android)
| +-> BGProcessingTask (iOS, requires charger + network)
|
+-> Must survive app termination (Android)?
| +-> Headless JS + enableHeadless: true + stopOnTerminate: false
|
+-> Must execute at exact time?
+-> Not possible with background tasks
+-> Use push notifications + server-side scheduling
| Feature | expo-background-task | expo-background-fetch | | ---------------- | ------------------------ | ----------------------- | | Status | Active (recommended) | Deprecated | | iOS API | BGTaskScheduler | Legacy Background Fetch | | Android API | WorkManager | JobScheduler | | Min interval | 15 minutes | ~10 minutes (advisory) | | Network required | Yes (by default) | No | | Reliability | Higher | Lower |
| Constraint | iOS | Android |
| -------------------- | --------------------------------------------- | ---------------------------------- |
| Refresh task time | ~30 seconds | ~10 minutes |
| Processing task time | Several minutes (charger required) | ~10 minutes |
| Minimum interval | 15 minutes (system-managed) | 15 minutes (WorkManager-enforced) |
| After force-quit | No tasks run | Headless JS can run (with config) |
| After reboot | Tasks resume automatically | Requires startOnBoot: true |
| Simulator support | No (physical device only for BGTaskScheduler) | Partial (Doze may not be enforced) |
</decision_framework>
<red_flags>
High Priority Issues:
finish(taskId) or returning a result -- the OS will penalize your app, reducing future scheduling frequency or killing the tasksetTimeout/setInterval for background work -- these are killed immediately when the app is backgroundedMedium Priority Issues:
getStatusAsync() before registering -- background tasks may be restricted by user settings or device stateGotchas & Edge Cases:
expo-background-task requires network connectivity by default -- tasks won't run offlineexpo-background-fetch is deprecated in favor of expo-background-task -- migrate to the new APIisTaskRegisteredAsync before re-registering</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST define tasks in the top-level scope (global) -- tasks defined inside React components or lifecycle methods will NOT be registered when the app starts from the background)
(You MUST call finish(taskId) or return a BackgroundTaskResult when task execution completes -- failing to signal completion causes the OS to penalize or kill your app)
(You MUST request background permissions explicitly on both platforms -- iOS requires Info.plist UIBackgroundModes entries, Android requires manifest permissions)
(You MUST handle the OS killing your task at any time -- use expiration listeners on iOS and timeout callbacks on Android to clean up gracefully)
(You MUST keep background work minimal -- sync only changed data, avoid heavy computation, respect the ~30s iOS refresh limit)
Failure to follow these rules will result in tasks that never execute, apps penalized by the OS scheduler, or apps rejected from the App Store for excessive background resource usage.
</critical_reminders>
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Xquik REST API patterns for X post search, user and timeline reads, cursor pagination, media downloads, monitors, signed webhooks, and approval-gated X actions
development
Mapbox GL JS interactive maps - map initialization, markers, popups, sources, layers, expressions, clustering, 3D terrain, geocoding, directions
tools
Leaflet interactive maps - map setup, tile layers, markers, popups, GeoJSON, custom controls, plugins, clustering, events