Skip to content

Task manager

Run your code when the OS wakes the app in the background: a location update, a geofence event, a periodic sync, a silent push. @symbiote-native/task-manager wraps expo-task-manager so every SymbioteNative adapter can define and inspect those tasks. It does not itself schedule anything: it defines tasks, tracks which are registered, dispatches native’s task-execute event to the matching executor, and acks completion. Starting a task running (periodic scheduling, geofence triggers, push delivery) is each consumer package’s own job: @symbiote-native/location, @symbiote-native/background-fetch, @symbiote-native/background-task, and @symbiote-native/notifications all register through it.

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/task-manager

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

expo-task-manager, unimodules-app-loader, 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.

defineTask must run at the top of the JS bundle, outside any component — the app can be launched headlessly to run a background task, with no views mounted, so a task defined inside a component lifecycle method would never register on that launch.

// index.ts, alongside AppRegistry.registerComponent — identical on every adapter
import { defineTask } from '@symbiote-native/task-manager';
const SYNC_TASK = 'background-sync';
defineTask(SYNC_TASK, async ({ data, error }) => {
if (error) {
console.error('background-sync failed:', error);
return;
}
await runSync(data);
});

A consumer package registers the task with native through its own native module — location’s startLocationUpdatesAsync/startGeofencingAsync, background-fetch’s registerTaskAsync, background-task’s registerTaskAsync, or notifications’s registerTaskAsync. Once registered, inspect it from here:

import {
getRegisteredTasksAsync,
isTaskRegisteredAsync,
unregisterTaskAsync,
} from '@symbiote-native/task-manager';
const isRunning = await isTaskRegisteredAsync(SYNC_TASK);
const tasks = await getRegisteredTasksAsync();
await unregisterTaskAsync(SYNC_TASK); // stop receiving updates for this task

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

Signature Description
defineTask(taskName, taskExecutor): void Defines the code that runs for a task. Call it at module scope
isTaskDefined(taskName): boolean Whether a task with this name is defined in the current bundle
isTaskRegisteredAsync(taskName): Promise<boolean> Whether a consumer has registered the task with native
getTaskOptionsAsync(taskName): Promise<TOptions> The options the task was registered with
getRegisteredTasksAsync(): Promise<ITaskManagerTask[]> Every registered task, with its name, type and options
unregisterTaskAsync(taskName): Promise<void> Stops delivery for one task
unregisterAllTasksAsync(): Promise<void> Stops delivery for every task
isAvailableAsync(): Promise<boolean> Whether the task manager can be used at all

taskExecutor is called with an ITaskManagerTaskBody:

Field Type Description
data TData The payload. Its shape depends on the consumer (locations, a geofence event)
error ITaskManagerError | null The failure (code, message) if the task failed, otherwise null
executionInfo.taskName string Name of the task that ran
executionInfo.eventId string Unique ID of this task event
executionInfo.appState 'active' | 'background' | 'inactive' | undefined App state when the task ran. iOS only

There is no free registerTaskAsync here — registration is always driven by the consumer package that needs the task, never by task-manager itself.

  • A defined task that native fires but nobody registered still gets acked and unregistered. If defineTask for a given name was renamed or deleted from the bundle after the task was registered, the dangling native registration is cleaned up automatically instead of leaking a wakelock forever.
  • A task executor that throws still acks native. The failure is logged, but the completion ack always fires, so a bug in one task’s executor can’t leave the OS believing the task never completed.

My task never runs. First check defineTask runs at the top of the bundle, outside any component, before anything registers the task. Registering a task that is not defined throws. Then check that a consumer package actually registered it: defineTask alone schedules nothing.

It stops when I swipe the app away. Background work is stopped when the user kills the app, though the platforms differ. On iOS swiping the app out of the switcher terminates it. On Android, removing it from recents does not always stop it, and the result varies by device vendor.

It works for 5 to 10 minutes on Android, then stops. Doze mode puts the device to sleep. For continuous tracking, use a foreground service (for example the foregroundService option of location updates) rather than relying on a periodic wake-up.

It works in a debug build but not in release. Registration or delivery can differ in release builds on Android. Test the release build early, and watch for a task that was renamed or removed: a dangling registration is cleaned up the next time native fires it.

Does it run on a simulator? Background execution is only reliable on physical devices, and the iOS Simulator does not support the iOS background task scheduler.

How do I debug a task? Log at the top of the executor and inside the error branch, and call getRegisteredTasksAsync() to see what native has registered.

Sources: Expo docs: BackgroundTask, Expo docs: BackgroundFetch, expo/expo#9570 Expo keeps stopping when the app is killed during background location, expo/expo#3535 background geolocation task disappears when the app is killed on Android, expo/expo#14076 task does not fire after 5 to 10 minutes on Android, expo/expo#26717 task is not defined, define it before registering.