Run JavaScript on a schedule in the background, even when your app isn't running.
This React Native for Vega app (SDK 2.0, RN 0.83) uses the Vega Task Manager to schedule a repeating headless JS task. Task Manager launches the task at the requested interval, whether or not the app is open, and keeps the schedule after the app exits.
Task Manager has no JavaScript API in SDK 2.0. The only JS scheduler,
@amazon-devices/kepler-epg-sync-scheduler, is limited to Live TV EPG sync. This sample includes a
small C++ Turbo Module that exposes Task Manager to JS, so any app can schedule its own periodic work.
Use it for work that should stay fresh without the user opening the app, for example refreshing cached content, syncing settings, or prefetching data for the next launch.
-
Open the app and press Schedule (60s). The task runs once right away, then every 60 seconds.
-
Close the app.
-
Come back a few minutes later. The app shows the task's state and every run that happened while it was closed:
On launch: getState('periodicDemo') -> PENDING, 4 runs, latest 2026-09-30T16:02:09.523Z, ...
Verified on a Fire TV device running Vega OS 2.0.
- Vega SDK 2.0 or later. This sample requires SDK 2.0. Install the
Vega Developer Tools and make
sure
vegais on yourPATH. - Node.js 22.14 or later.
- A Vega device or the Vega Virtual Device.
- (Optional) The Vega Studio extension for Visual Studio Code, if you prefer an IDE to the command line. See Set up the Vega Studio extension.
You can build and run this sample from the command line with the vega CLI, or from Visual Studio
Code with the Vega Studio extension. The steps below use the command line. To use the IDE instead,
install the extension (see the link above), open this folder in Visual Studio Code, and follow the
Vega Studio documentation to build the app, start the Vega Virtual Device or select your device, and
run the app. Run npm install first in either case, because it also installs the Turbo Module's
dependencies.
npm install # also installs the Turbo Module's dependencies (turbo-module/)
npm test # Turbo Module tests, then app tests
npm run build # builds the Turbo Module (C++, all architectures), then the app (Release)npm run build writes one vpkg per architecture to build/<arch>-release/. Install the one that
matches the device's userspace architecture:
vega run-app build/armv7-release/periodictasksample_armv7.vpkg # most Fire TV devices
vega run-app build/aarch64-release/periodictasksample_aarch64.vpkg # 64-bit userspace, Vega Virtual Device on Apple silicon Macs
vega run-app build/x86_64-release/periodictasksample_x86_64.vpkg # Vega Virtual Device on Intel Macs and x86_64 LinuxMany Fire TV devices run a 32-bit (armv7) userspace on 64-bit hardware. The Vega Virtual Device
uses the host's CPU architecture, so on an Apple silicon Mac (M1 and later) it needs the
aarch64 vpkg, not x86_64. If you install the wrong one, vega run-app still reports success,
but the app crashes on launch with TurboModuleRegistry.getEnforcing(...): 'PeriodicTaskScheduler' could not be found.
Use npm run build:debug (output in build/<arch>-debug/) to see the app's console.log output
in the device log. Release builds don't write it.
UI process Task Manager (system)
┌─────────────────────────────┐ ┌──────────────────────┐
│ App.tsx │ scheduleRepeating │ │
│ PeriodicTasks ──▶ C++ ────┼──────────────────▶│ every intervalSeconds│
│ (Turbo Module) │ ITaskManager:: │ app running or not │
└─────────────────────────────┘ submit() └──────────┬───────────┘
│ launches
Headless task process ▼
┌──────────────────────────────────────────────────────────────────────┐
│ manifest.toml [[components.task]] ──▶ task.js ──▶ src/PeriodicTask.ts │
│ PeriodicTasks. │
│ recordRun() │
└──────────────────────────────────────────────────────┬───────────────┘
▼
/data/periodic_task_runs.log ──▶ read by App.tsx on launch
Three pieces work together:
- A Turbo Module (
turbo-module/) wraps the Task Manager platform API (com.amazon.kepler.tasks) in C++ and exposes it to JS asPeriodicTasks. - A headless task component declared in
manifest.toml. Task Manager launches it on schedule.task.jsregisters its JS entry point,doPeriodicTaskinsrc/PeriodicTask.ts. - Shared state. The task runs in its own process, so it can't share memory with the UI. It
records each run through the Turbo Module, which stores runs in a file in the app's
/datadirectory. The UI reads them on launch.
vega-periodic-task-sample/
├── manifest.toml Declares the UI and the headless task component
├── task.js Registers the task's entry point (bundled as task.hermes.bundle)
├── index.js UI entry point
├── src/
│ ├── App.tsx UI: schedule, inspect and cancel the task, show run history
│ ├── components/ ActionButton (focusable button with TV focus styling)
│ ├── theme.ts TV font sizes, colors and safe-zone inset
│ ├── PeriodicTask.ts The background work (runs in the headless JS runtime)
│ └── taskConfig.ts Component id, task name and interval shared by UI and task
├── test/App.spec.tsx
└── turbo-module/ Local package "@amazon-devices/periodic-task-scheduler" (app dependency file:./turbo-module)
├── CMakeLists.txt Imports the com.amazon.kepler.tasks IDL
├── src/turbo-modules/
│ ├── PeriodicTaskScheduler.ts Turbo Module spec (input to codegen)
│ └── PeriodicTasks.ts Typed JS wrapper the app imports
├── kepler/turbo-modules/
│ ├── PeriodicTaskScheduler.cpp Task Manager calls
│ └── generated/ Codegen output, don't edit
└── test/PeriodicTasks.spec.ts
The Vega app build doesn't compile native code in the app project itself. It links native code from the app's npm dependencies. That's why the Turbo Module is its own local package. The root scripts build both, so you work with a single project.
turbo-module/kepler/turbo-modules/PeriodicTaskScheduler.cpp:
auto module = apmf::GetProcessObject()
->getComponent("/com.amazon.kepler.tasks.tasks_module")
.TryQueryInterface<IKeplerTasksModule>();
auto manager = module->makeTaskManager();
MonotonicDuration repeat{};
repeat.time.seconds = intervalSeconds;
auto timeProperties = module->makeTimePropertiesBuilder()->repeatInterval(repeat)->build();
auto request = module->makeTaskRequestBuilder()
->requireTime(timeProperties)
->repeating()
->persistAfterAppDeath() // keep the schedule after the app exits
->name(name) // unique per app; prevents duplicates
->build(componentId); // a [[components.task]] from manifest.toml
auto registration = manager->submit(request);The module also implements state queries, listing and cancelling with
tryGetTaskRegistrationByName(), getAllTaskRegistrationsForPackage() and
ITaskRegistration::remove().
manifest.toml:
[[components.task]]
id = "com.amazondeveloper.periodictasksample.periodicTask"
runtime-module = "/com.amazon.kepler.runtime.react_native_kepler_headless_4@IReactNativeKeplerHeadless_0"
[processes]
[[processes.group]]
component-ids = ["com.amazondeveloper.periodictasksample.main"]
[[processes.group]]
component-ids = ["com.amazondeveloper.periodictasksample.periodicTask"]A process group can't mix runtime modules, so the UI and the task each get their own.
task.js. The key is <component id>::doTask, and it must be registered in task.js:
HeadlessEntryPointRegistry.registerHeadlessEntryPoint2(
`${TASK_COMPONENT_ID}::doTask`,
() => doPeriodicTask,
);src/PeriodicTask.ts:
export const doPeriodicTask = async (): Promise<void> => {
try {
PeriodicTasks.recordRun(new Date().toISOString()); // replace with your work
} catch (e) {
console.error(`periodic task failed: ${(e as Error).message}`);
}
};import {PeriodicTasks} from '@amazon-devices/periodic-task-scheduler';
await PeriodicTasks.scheduleRepeating({
componentId: 'com.amazondeveloper.periodictasksample.periodicTask',
name: 'periodicDemo',
intervalSeconds: 60,
});
PeriodicTasks.getState('periodicDemo'); // 'PENDING' | 'RUNNING' | ... | 'NOT_FOUND'
PeriodicTasks.cancel('periodicDemo');| Method | Description |
|---|---|
scheduleRepeating({componentId, name, intervalSeconds, requireInternet?}) → Promise<string> |
Schedules a repeating task. Resolves with the task UUID. Rejects if intervalSeconds is under 60, name is empty or over 32 bytes, or a task with that name is already scheduled. With requireInternet, runs only while the device is online. |
getState(name) → TaskState |
PENDING, READY, RUNNING, CANCELED, FAILED, SUCCEEDED, or NOT_FOUND. |
getScheduledNames() → string[] |
Names of all tasks scheduled by the app. |
cancel(name) → boolean |
Removes the task. Returns false if it wasn't scheduled. |
cancelAll() → number |
Removes all of the app's tasks. Returns how many were removed. |
recordRun(timestamp), getRunHistory(), clearRunHistory() |
The sample's run log in /data, readable from both processes. Keeps the last 20 runs, newest first. |
Each run shows up in the device log as a launch of the task component, followed by its completion. These lines appear in Release builds too:
vda shell "loggingctl log -f" | grep -E 'periodicTask\): (Trigger event LAUNCH in state IDLE|Execution completed)'... appInst[2204](com.amazondeveloper.periodictasksample.periodicTask): Trigger event LAUNCH in state IDLE. Completed: true
... appInst[2204](com.amazondeveloper.periodictasksample.periodicTask): Execution completed: 0
... appInst[2205](com.amazondeveloper.periodictasksample.periodicTask): Trigger event LAUNCH in state IDLE. Completed: true
To close the app from the command line:
vda shell vlcm terminate-app --pkg-id com.amazondeveloper.periodictasksample- Copy
turbo-module/into your project and add it as a dependency:"@amazon-devices/periodic-task-scheduler": "file:./turbo-module". Add the MetroblockListentries frommetro.config.js. - Declare a
[[components.task]]and its own[[processes.group]]in yourmanifest.toml. - Register
<component id>::doTaskin atask.jsat the project root, with@amazon-devices/headless-task-manager. - Call
PeriodicTasks.scheduleRepeating()when the work should start, for example after sign-in. Schedule again after app updates (see below).
- Use the longest interval you can. 60 seconds is for the demo. The minimum is 60 seconds. Frequent background work costs device resources.
- Run times aren't exact. Task Manager may delay a run when the system is busy. If a run is still waiting when the next one is due, the older one is dropped.
- The interval follows system uptime. The countdown restarts after a power cycle.
- Installing a new build clears the schedule. After an update,
getState()returnsNOT_FOUND. Schedule again at startup, or from a task with[[tasks.work]] mode = "install"in the manifest. Uninstalling the app removes its tasks. - A run gets 5 minutes.
- The headless runtime is limited. Don't import UI components or UI APIs from
react-native. AsyncStorage isn't available in the RN 0.83 headless runtime (NativeModule: AsyncStorage is null), even though it works in the UI. Your own Turbo Modules do load there, which is how this sample shares state. - Don't let
doTaskreject. With@amazon-devices/headless-task-manager1.2.8 on OS 2.0, a rejecteddoTasknever completes and the task staysRUNNING. Catch errors insidedoTask. - Some Task Manager features aren't implemented in SDK 2.0 and throw
NotSupportedError: observers and results, success and failure counts, custom retry policies,requireDeviceIdle(), input payloads, andupdate(). To change the interval, cancel and schedule again. - Boot-triggered tasks (
requireReboot()) need a first-party-only privilege and aren't available to third-party apps.
The full Task Manager API is documented in the SDK, in tasks.idl and tasks-cookbook.md in the
KeplerAppInterface package.
| Symptom | Cause and fix |
|---|---|
App crashes on launch with TurboModuleRegistry.getEnforcing(...): 'PeriodicTaskScheduler' could not be found |
The installed vpkg doesn't match the device's userspace architecture. Most Fire TV devices need armv7, even on 64-bit hardware. The Vega Virtual Device needs aarch64 on Apple silicon Macs and x86_64 on Intel hosts. Install the matching vpkg from build/<arch>-release/. See Quick start to check the architecture. |
No [PeriodicTaskSample] lines in the device log |
Release builds don't write console.log output. Use npm run build:debug, or check the Task Manager launch lines shown in Checking it on a device. |
scheduleRepeating rejected -> A task named 'periodicDemo' is already scheduled |
The task is already scheduled. Press Cancel, then Schedule again. |
getState() returns NOT_FOUND after installing a new build |
Installing a build clears the schedule. Press Schedule again. |
Task stays RUNNING and never runs again |
doTask rejected. Catch all errors inside it (see Things to know). |
- Edit
turbo-module/src/turbo-modules/PeriodicTaskScheduler.ts. - Run
npm --prefix turbo-module run kepler-codegen. This regeneratesgenerated/and overwritesPeriodicTaskScheduler.{h,cpp}. Back up your implementation first and merge it back in afterwards. - Update
PeriodicTasks.tsandturbo-module/src/turbo-modules/__mocks__/PeriodicTaskScheduler.ts. - Run
npm run build.
See CONTRIBUTING.md and the Code of Conduct.
See SECURITY.md. Please don't report security issues in public GitHub issues.
This sample is licensed under the MIT-0 License. See LICENSE.