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.
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.
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.
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.
| Methods | Type | Description |
|---|---|---|
defineTask(identifier: string, handler: BackgroundTaskHandler) => () => voidRegister a handler. Returns an idempotent remover. | (identifier: string, handler: BackgroundTaskHandler) => () => void | Register 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) => void | Drop the pending request for an identifier. |
| Invocation | Type | Description |
|---|---|---|
executionIdstringThis launch. Pass it to no API; it keys expiry. | string | This launch. Pass it to no API; it keys expiry. |
identifierstringThe submitted identifier. | string | The submitted identifier. |
kindBackgroundTaskKindRefresh or processing. | BackgroundTaskKind | Refresh or processing. |
| Pending task | Type | Description |
|---|---|---|
identifierstringThe submitted identifier. | string | The submitted identifier. |
kindBackgroundTaskKindRefresh or processing. | BackgroundTaskKind | Refresh or processing. |
earliestBeginDateMsnumberEarliest start, when one was set. | number | Earliest start, when one was set. |
requiresNetworkConnectivitybooleanWhether it waits for connectivity. | boolean | Whether it waits for connectivity. |
requiresExternalPowerbooleanWhether it waits for charging. | boolean | Whether it waits for charging. |
The handler also receives signal: AbortSignal alongside the invocation
fields. It aborts when the system expires the launch.
| iOS | Android | Web | |
|---|---|---|---|
| Scheduler | Task scheduler | Work manager | None, inert |
| Launch | Setup file JS | Headless JS task | Never |
| Refresh | Short fetch | One-time work | Validates only |
| Processing | Needs declaration | Needs declaration | Validates only |
| Requires | OS decides time | Constraints decide | No effects |
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.
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.
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.