One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Background Tasks

Schedule refresh and processing work the OS runs behind your app

One.BackgroundTasks schedules work the operating system runs when your app is not in front: short refresh jobs that fetch content for the next launch, and longer processing jobs for maintenance gated on network or charging. Both platforms decide when a submitted job runs; a submitted request is not a timer. For CPU work while the app is alive, see background computation instead.

On iOS jobs run through BGTaskScheduler, with handlers launched from the native setup file. On Android they run as uniquely-named one-time WorkManager requests that boot the app’s ReactHost and invoke the same setup-file handlers through a Headless JS task. On web registration and submission validate but have no system effects.

Usage

one({
setupFile: { native: './background-setup.ts' },
native: {
app: {
ios: {
bundleId: 'com.example.myapp',
backgroundTasks: {
refresh: ['com.example.myapp.refresh'],
processing: ['com.example.myapp.maintenance'],
},
},
},
},
})

Declare each identifier once as refresh or processing. Prebuild stamps them into BGTaskSchedulerPermittedIdentifiers and the matching background mode on iOS, and into the Android manifest where the WorkManager half reads them.

background-setup.ts
import { One } from 'one'
One.BackgroundTasks.defineTask('com.example.myapp.refresh', async ({ signal }) => {
const response = await fetch('https://example.com/feed', { signal })
await response.text()
})

Define every handler at module scope in the native setup file, which loads when a background launch starts JavaScript without opening a window. The handler runs once per system launch: resolving reports success, throwing reports failure.

import { One } from 'one'
await One.BackgroundTasks.submit('com.example.myapp.refresh', {
earliestBeginDateMs: Date.now() + 15 * 60_000,
})
await One.BackgroundTasks.submit('com.example.myapp.maintenance', {
requiresNetworkConnectivity: true,
requiresExternalPower: true,
})
const pending = await One.BackgroundTasks.getPending()
One.BackgroundTasks.cancel('com.example.myapp.refresh')

Submit from foreground JavaScript after defining handlers. Submitting an identifier again replaces its pending request.

How it works

Refresh jobs are short fetches; processing jobs are longer work that can require connectivity, charging, or both. Requirements apply only to processing jobs: a refresh submit carrying them rejects, on both platforms. earliestBeginDateMs sets the earliest start as a Unix timestamp. Unknown identifiers reject with E_BACKGROUND_TASK_INPUT; scheduler failures reject with E_BACKGROUND_TASK_SUBMIT. iOS may defer or decline execution by system conditions and app usage, and submitting the same identifier replaces the pending request rather than queueing.

When the OS launches a job, native code invokes the registered handler with the execution id, identifier, kind, and an abort signal. Check the signal during work so expiration stops it promptly: when iOS expires a task its signal aborts and One reports failure to the system. On Android expiration aborts the same way, and a launch whose handler never registered fails after a startup grace period. defineTask throws for a duplicate identifier and returns an idempotent remover. Schedule the next run from the handler or the foreground app when you need recurring work.

Android constraints mirror the options: network connectivity becomes a connected-network constraint, external power a charging constraint. WorkManager persists requests across process death and reboot, and runs them when constraints allow. A force-stop prevents background execution until the user opens the app again.

API reference

MethodsTypeDescription
defineTask(identifier: string, handler: BackgroundTaskHandler) => () => voidRegister a handler. Returns an idempotent remover.
(identifier: string, handler: BackgroundTaskHandler) => () => voidRegister a handler. Returns an idempotent remover.
submit(identifier: string, options: { earliestBeginDateMs?: number; requiresNetworkConnectivity?: boolean; requiresExternalPower?: boolean }) => Promise<void>Schedule a job, replacing any pending one.
(identifier: string, options: { earliestBeginDateMs?: number; requiresNetworkConnectivity?: boolean; requiresExternalPower?: boolean }) => Promise<void>Schedule a job, replacing any pending one.
getPending() => Promise<PendingBackgroundTask[]>The scheduled requests the system still holds.
() => Promise<PendingBackgroundTask[]>The scheduled requests the system still holds.
cancel(identifier: string) => voidDrop the pending request for an identifier.
(identifier: string) => voidDrop the pending request for an identifier.
InvocationTypeDescription
executionIdstringThis launch. Pass it to no API; it keys expiry.
stringThis launch. Pass it to no API; it keys expiry.
identifierstringThe submitted identifier.
stringThe submitted identifier.
kindBackgroundTaskKindRefresh or processing.
BackgroundTaskKindRefresh or processing.
Pending taskTypeDescription
identifierstringThe submitted identifier.
stringThe submitted identifier.
kindBackgroundTaskKindRefresh or processing.
BackgroundTaskKindRefresh or processing.
earliestBeginDateMsnumberEarliest start, when one was set.
numberEarliest start, when one was set.
requiresNetworkConnectivitybooleanWhether it waits for connectivity.
booleanWhether it waits for connectivity.
requiresExternalPowerbooleanWhether it waits for charging.
booleanWhether it waits for charging.

The handler also receives signal: AbortSignal alongside the invocation fields. It aborts when the system expires the launch.

Platform differences

iOSAndroidWeb
SchedulerTask schedulerWork managerNone, inert
LaunchSetup file JSHeadless JS taskNever
RefreshShort fetchOne-time workValidates only
ProcessingNeeds declarationNeeds declarationValidates only
RequiresOS decides timeConstraints decideNo effects

Limits and gotchas

Examples

Refresh a feed in the background

background-setup.ts
import { One } from 'one'
One.BackgroundTasks.defineTask('com.example.myapp.refresh', async ({ signal }) => {
const response = await fetch('https://example.com/feed', { signal })
const text = await response.text()
await One.FileSystem.writeFile(`${One.FileSystem.getDirectories().cache}/feed.json`, text)
await One.BackgroundTasks.submit('com.example.myapp.refresh', {
earliestBeginDateMs: Date.now() + 6 * 3_600_000,
})
})

The handler persists the fetch where the next foreground launch reads it, then chains the following run while it still has execution time. The shared abort signal cancels a slow fetch on expiry.

Run maintenance on power

import { One } from 'one'
async function scheduleMaintenance() {
await One.BackgroundTasks.submit('com.example.myapp.maintenance', {
requiresNetworkConnectivity: true,
requiresExternalPower: true,
})
const pending = await One.BackgroundTasks.getPending()
return pending.some((task) => task.identifier === 'com.example.myapp.maintenance')
}

Processing constraints keep heavy work off battery and offline stretches. Reading the pending list back confirms the request landed.

Define one handler per file

background-setup.ts
import { One } from 'one'
import { refreshFeed } from './tasks/refresh-feed'
import { runMaintenance } from './tasks/maintenance'
const stopRefresh = One.BackgroundTasks.defineTask('com.example.myapp.refresh', refreshFeed)
const stopMaintenance = One.BackgroundTasks.defineTask(
'com.example.myapp.maintenance',
runMaintenance
)
export function teardownBackgroundTasks() {
stopRefresh()
stopMaintenance()
}

Each identifier maps to exactly one handler function, so splitting handlers into modules keeps the setup file a flat registry. The removers release definitions in tests without touching the scheduler.

Edit this page on GitHub.