Reminders
A reminder is a persistent timer. It survives deactivation and restarts, and activates the actor when it is due. For a timer that only lives as long as the activation, see Timers.
public class Billing : Actor<BillingState>, IBilling, IRemindable{ public Task Start() => RegisterReminderAsync("invoice", TimeSpan.FromHours(1), TimeSpan.FromDays(1)).AsTask();
public ValueTask ReceiveReminderAsync(ReminderTick tick, CancellationToken ct) { var late = tick.FiredAt - tick.DueAt; ... }}- The actor must implement
IRemindable. Registering an existing name replaces the reminder. - A
nullperiod fires once, then the reminder is removed. UnregisterReminderAsync(name)removes one, andGetRemindersAsync()lists this actor’s reminders.- While the app runs, reminders fire on time.
- Missed ticks (the app wasn’t running) coalesce into one, and
tick.DueAttells you how late it is. - At least once. A reminder only moves on after its actor handled it, and a failure is retried within a minute.
Reminders persist in the IActorReminderStore. UseFileStorage(dir) keeps them in dir/reminders.json, and
Shiny.DocumentDb keeps them alongside state.
In the background
Section titled “In the background”On a phone, the OS suspends or kills apps, so a reminder can come due while nothing is running. Add
Shiny.Actors.Jobs:
dotnet add package Shiny.Actors.Jobsbuilder.Services.AddShinyActors(x => x.UseBackgroundReminders()); // a Shiny.Jobs IJob: BGTaskScheduler / WorkManagerThe job fires everything that’s due, then deactivates the actors it woke so their state is written before the app is suspended again.
On time, even when the app is dead
Section titled “On time, even when the app is dead”Add Shiny.Actors.Notifications and give the reminder a notification:
dotnet add package Shiny.Actors.Notificationsbuilder.Services.AddShinyActors(x => x.UseReminderNotifications()); // registers Shiny.Notifications too
await RegisterReminderAsync("standup", TimeSpan.FromHours(1), notification: new ReminderNotification("Standup", "Starts in 5 minutes") { Channel = "alerts" });Each such reminder becomes an OS-scheduled local notification on iOS, Android, Mac and Windows:
- It’s moved when the reminder advances, cancelled when it’s removed, and re-synced at startup.
- The OS shows it on time whether or not the app is running.
- Tapping it opens the app, where the overdue reminder fires.
Channel is an Android notification channel the app has created. Without it, the default channel is used.
Observing reminders
Section titled “Observing reminders”IActorReminderObserver is told about every reminder that is scheduled (registered, advanced, or loaded at startup)
and every one that is removed. That’s how the notifications package works, and you can use it to mirror reminders
anywhere else:
actors.AddReminderObserver<CalendarMirror>(); // created by the container
public sealed class CalendarMirror : IActorReminderObserver{ public ValueTask OnScheduledAsync(ActorReminder reminder, CancellationToken ct) { ... } public ValueTask OnRemovedAsync(ActorReminder reminder, CancellationToken ct) { ... }}Calls must be idempotent. A failure is logged and never thrown.


