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 |
Installation
Section titled “Installation”npm install @symbiote-native/task-managerScaffolding 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 adapterimport { 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 taskIdentical import surface on every adapter — @symbiote-native/task-manager/react, /vue,
/svelte, /solid, /angular all re-export the same functions.
Functions
Section titled “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 |
What the executor receives
Section titled “What the executor receives”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
defineTaskfor 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.
Common questions
Section titled “Common questions”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.