Message scheduling
Fluxzero allows scheduling messages for future delivery using the MessageScheduler.
Here’s an example that schedules work 30 days after an account closes and cancels it if the account reopens:
class UserLifecycleHandler { @HandleEvent void handle(AccountClosed event) { Fluxzero.schedule( new TerminateAccount(event.getUserId()), "AccountClosed-" + event.getUserId(), Duration.ofDays(30) ); }
@HandleEvent void handle(AccountReopened event) { Fluxzero.cancelSchedule("AccountClosed-" + event.getUserId()); }
@HandleSchedule void handle(TerminateAccount schedule) { // Recheck that the account is still closed, then perform termination }}class UserLifecycleHandler {
@HandleEvent fun handle(event: AccountClosed) { Fluxzero.schedule( TerminateAccount(event.userId), "AccountClosed-${event.userId}", Duration.ofDays(30) ) }
@HandleEvent fun handle(event: AccountReopened) { Fluxzero.cancelSchedule("AccountClosed-${event.userId}") }
@HandleSchedule fun handle(schedule: TerminateAccount) { // Recheck that the account is still closed, then perform termination }}Scheduling commands
Section titled “Scheduling commands”You can also schedule commands directly using scheduleCommand(...).
class UserLifecycleHandler { @HandleEvent void handle(AccountClosed event) { Fluxzero.scheduleCommand( new TerminateAccount(event.getUserId()), "AccountClosed-" + event.getUserId(), Duration.ofDays(30)); }
@HandleEvent void handle(AccountReopened event) { Fluxzero.cancelSchedule("AccountClosed-" + event.getUserId()); }}class UserLifecycleHandler {
@HandleEvent fun handle(event: AccountClosed) { Fluxzero.scheduleCommand( TerminateAccount(event.userId), "AccountClosed-${event.userId}", Duration.ofDays(30) ) }
@HandleEvent fun handle(event: AccountReopened) { Fluxzero.cancelSchedule("AccountClosed-${event.userId}") }}Schedule identity and lifecycle
Section titled “Schedule identity and lifecycle”Use ScheduleId when different schedule categories can share the same domain ID. Fluxzero uses its stable type:id
representation for scheduling, lookup and cancellation.
ScheduleId expiryId = ScheduleId.of("account-expiry", userId);Fluxzero.schedule(new TerminateAccount(userId), expiryId, Duration.ofDays(30));Fluxzero.cancelSchedule(expiryId);val expiryId = ScheduleId.of("account-expiry", userId)Fluxzero.schedule(TerminateAccount(userId), expiryId, Duration.ofDays(30))Fluxzero.cancelSchedule(expiryId)Work tied to a Model
Section titled “Work tied to a Model”Declare @Parent on a scheduled payload to cancel pending work when its committed Model owner is deleted.
See Parent-owned schedules.
Periodic scheduling
Section titled “Periodic scheduling”Fluxzero supports recurring message schedules via the @Periodic annotation. This makes it easy to run background tasks on a fixed interval or cron schedule.
You can apply @Periodic to a schedule payload or a @HandleSchedule method.
@Periodic(delay = 5, timeUnit = TimeUnit.MINUTES)public record RefreshData(String index) {}@Periodic(cron = "0 0 * * MON", timeZone = "Europe/Amsterdam")@HandleSchedulevoid weeklySync(PollData schedule) { ...}@Periodic(delay = 5, timeUnit = TimeUnit.MINUTES)data class RefreshData(val index: String)@Periodic(cron = "0 0 * * MON", timeZone = "Europe/Amsterdam")@HandleSchedulefun weeklySync(schedule: PollData) { ...}Initial autostart timing
Section titled “Initial autostart timing”initialDelay defaults to -1, which means no explicit initial delay was configured. Compatibility defaults treat that
implicit value as 0, so an auto-started periodic schedule starts immediately. New defaults can be selected with:
fluxzero.defaults.version=2026.05.21# equivalent explicit setting:fluxzero.scheduling.periodic.useDefaultInitialDelay=trueWith this behavior, fixed-delay schedules first run after delay, and cron schedules first run at the next cron match.
For example, @Periodic(delay = 60_000) first runs after 60 seconds, while
@Periodic(cron = "*/5 * * * *") first runs at the next five-minute boundary. Set initialDelay = 0 when the first
run should be immediate.
Behavior and advanced options
Section titled “Behavior and advanced options”@Periodiconly applies to scheduled messages (used with@HandleSchedule)- The schedule automatically reschedules itself after every execution unless cancelled
- You can:
- Return
voidornullto use the same delay next time - Return a
DurationorInstantto customize the next deadline - Return a new
Schedulepayload to completely redefine the next cycle
- Return
- On error:
- The default is to continue (
continueOnError = true) - Use
delayAfterErrorto delay retries after failure - Throw
CancelPeriodicto stop the schedule completely
- The default is to continue (
- Use
@Periodic(autoStart = false)to prevent the schedule from activating on startup - The schedule ID defaults to the payload class name but can be customized using
scheduleId
Example: polling with error fallback
Section titled “Example: polling with error fallback”@Periodic(delay = 60, timeUnit = TimeUnit.MINUTES, delayAfterError = 10)@HandleSchedulevoid pollExternalService(PollTask pollTask) { try { externalService.fetchData(); } catch (Exception e) { log.warn("Polling failed, will retry in 10 minutes", e); throw e; }}@Periodic(delay = 60, timeUnit = TimeUnit.MINUTES, delayAfterError = 10)@HandleSchedulefun pollExternalService(task: PollTask) { try { externalService.fetchData() } catch (e: Exception) { log.warn("Polling failed, will retry in 10 minutes", e) throw e }}In this example:
- The task runs every hour
- If it fails, it retries after 10 minutes
- If it succeeds, it returns to the hourly schedule
Delivery and completion
Section titled “Delivery and completion”SDK 2.x scheduling and cancellation convenience methods return without waiting for a remote acknowledgement.
When you need confirmation that a schedule was stored, use the future-returning overload, for example
messageScheduler.schedule(schedule, false, Guarantee.STORED), and await its future. This confirms storage,
not execution of the delayed action.
See Sending messages for shared delivery settings and MessageScheduler for the exact overloads and completion contracts.
© 2026 Fluxzero