Skip to content

Background task

Sync, clean up or upload while the app is closed, when the OS decides the moment is right. @symbiote-native/background-task wraps expo-background-task so every SymbioteNative adapter can register a task that runs in the background on the OS’s own schedule: BGTaskScheduler on iOS, WorkManager on Android. It is the modern replacement for @symbiote-native/background-fetch. It does not define what the task does: define it first with @symbiote-native/task-manager’s defineTask, then register it here.

Every export is a plain function or a one-time module-load side effect — no hook/composable/ service to wrap, so every adapter’s entry point is a plain re-export of the same core.

OS platform Support
iOS ✅ live
Android ✅ live
Framework adapter Support
React ✅ live
Vue ✅ live
Angular ✅ live
Svelte ✅ live
Solid ✅ live
Terminal window
npm install @symbiote-native/background-task @symbiote-native/task-manager

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --background-task (or add --background-task in an existing app) installs and wires this for you — see @symbiote-native/cli.

expo-background-task and expo-modules-core come along as regular dependencies, pinned to exact versions — never install them yourself, and never add the expo meta-package to your project.

// index.ts, alongside AppRegistry.registerComponent — identical on every adapter
import { defineTask } from '@symbiote-native/task-manager';
import { registerTaskAsync, BackgroundTaskResult } from '@symbiote-native/background-task';
const SYNC_TASK = 'background-sync';
defineTask(SYNC_TASK, async () => {
try {
await runSync();
return BackgroundTaskResult.Success;
} catch (error) {
console.error('background-sync failed:', error);
return BackgroundTaskResult.Failed;
}
});
// Somewhere after the task is defined (a settings screen, app bootstrap, …):
await registerTaskAsync(SYNC_TASK, { minimumInterval: 15 });
import {
getStatusAsync,
unregisterTaskAsync,
addExpirationListener,
} from '@symbiote-native/background-task';
const status = await getStatusAsync();
// iOS only — the system can interrupt a running background task before it finishes.
const subscription = addExpirationListener(() => {
console.warn('background-sync was interrupted before it finished');
});
subscription.remove();
await unregisterTaskAsync(SYNC_TASK); // stop receiving executions of it

Identical import surface on every adapter — @symbiote-native/background-task/react, /vue, /svelte, /solid, /angular all re-export the same functions.

Signature Description
registerTaskAsync(taskName, options?): Promise<void> Registers a defined task. Throws if the task is not defined; skips if already registered
unregisterTaskAsync(taskName): Promise<void> Stops executions of the task
getStatusAsync(): Promise<BackgroundTaskStatus> Whether background tasks are available on this device
addExpirationListener(listener): { remove } iOS only. Calls listener when the system interrupts a running task before it finishes
triggerTaskWorkerForTestingAsync(): Promise<boolean> Runs the task worker now, for testing. Resolves false outside a dev build
Field Type Description
minimumInterval number | undefined Inexact interval in minutes between runs. Defaults to 12 hours; the minimum is 15 minutes. The OS treats it as a minimum delay only, and on iOS a short interval is often ignored
Enum Values
BackgroundTaskResult What the task executor returns: Success or Failed
BackgroundTaskStatus Restricted (unavailable, for example on an iOS Simulator) or Available
  • registerTaskAsync requires the task to already be defined — it throws if the task named isn’t defined yet with @symbiote-native/task-manager’s defineTask.
  • The iOS Simulator cannot run it. It has no BGTaskScheduler support, so the status reads Restricted and registration is skipped. Test on a device.
  • registerTaskAsync is a no-op, twice over. It skips silently (one-time console warning) when the status reads Restricted, and it skips again, quietly, when the task is already registered.
  • triggerTaskWorkerForTestingAsync only runs in a dev build — it always resolves false in production.
  • Nothing runs on the iOS Simulator. Background tasks need a physical device.
  • triggerTaskWorkerForTestingAsync does nothing on iOS. BGTaskSchedulerPermittedIdentifiers must contain com.expo.modules.backgroundtask.processing in Info.plist; rebuild after adding it.
  • minimumInterval. A hint only; the OS decides when to run.
  • Status ignores the Background App Refresh setting on iOS. Reported upstream: the status call may not read the real permission, so do not rely on it alone.

Sources: Expo docs: BackgroundTask, expo/expo#40440, expo/expo#48786, expo/expo#35350.