Handoffs and actions
Knowing why your app opened, even when it was killed when the alarm fired.
An alarm frequently fires while your JS runtime is not running. The package therefore records everything natively, on disk, before any JavaScript executes — and exposes three read paths.
The three sources
| Source | What it holds | Use it for |
|---|---|---|
getPendingNativeAlarmHandoffAsync() | A single durable slot: the latest native handoff | App-launch routing |
getPendingAlarmActionsAsync() | The full history of native action records | Reconciling what happened |
getCurrentAlarmContextAsync() | The alarm that is alerting or recently fired | Resume routing |
getPendingNativeAlarmHandoffAsync() is the one you want for routing. On iOS it is written by the
AlarmKit App Intent when the user presses a button. On Android it is written the moment the alarm
starts ringing, so a cold-launched app can route straight to its alarm UI without waiting for a JS
event.
The launch pattern
Run this on cold launch and on every foreground:
async function reconcileAlarms() {
const handoff = await AlarmScheduler.getPendingNativeAlarmHandoffAsync();
const context = handoff ? null : await AlarmScheduler.getCurrentAlarmContextAsync();
const alarmId = handoff?.alarmId ?? context?.id;
if (!alarmId) return;
if (handoff?.action === 'nativeStop') {
// The user pressed the system stop control. Not proof of completion —
// re-arm before presenting UI.
await AlarmScheduler.scheduleNativeAlarmBackupAsync(alarmId, 0.1);
}
router.replace(`/alarm/${alarmId}`);
await AlarmScheduler.clearPendingNativeAlarmHandoffAsync();
}nativeStop means the user pressed the system alarm stop control. Treat it as a bypass signal, not
as successful completion.
clearPendingNativeAlarmHandoffAsync() clears only the durable slot. It does not clear the full
action history — use clearPendingAlarmActionsAsync(ids?) for that.
Action records
const actions = await AlarmScheduler.getPendingAlarmActionsAsync();Each record carries an action of nativeStop, secondaryOpen, snooze or dismiss, plus
context about what the package did in response — whether it asked iOS to foreground the app
(foregroundRequested), whether it re-armed (rescheduled, backupAlarmId, backupScheduledFor).
Two fields are Android-only:
trigger: trueon asecondaryOpenrecord means it was written by the alarm firing, not by a user tap.timedOut: trueon adismissrecord means the ring hitmaxRingDurationSeconds.
Clear records once consumed:
await AlarmScheduler.clearPendingAlarmActionsAsync(actions.map((a) => a.id));
await AlarmScheduler.clearPendingAlarmActionsAsync(); // or clear everythingEvents are a bonus, not the mechanism
const subscription = AlarmScheduler.addListener('onAlarmTriggered', (alarm) => {
console.log(alarm);
});
subscription.remove();There are three: onAlarmTriggered, onAlarmAction and onAlarmStateChange. They are delivered only
when a JS runtime happens to be alive.
Everything they carry is also persisted natively, so the reliable pattern on both platforms is: handle the event if it arrives, and reconcile from the async getters on every launch and foreground. Never rely on an event alone.