Parent-owned schedules
A reminder should stop when the thing it reminds you about is deleted. Mark a scheduled payload’s owner with
@Parent, and Fluxzero cancels its stored schedules when that Model is deleted. Your application does not need a
separate cleanup consumer.
record RunReminder(@Parent ReminderId reminderId, Instant expectedDeadline) {}
Fluxzero.scheduleCommand(new RunReminder(reminderId, deadline), "reminder-" + reminderId, deadline);data class RunReminder(@Parent val reminderId: ReminderId, val expectedDeadline: Instant)
Fluxzero.scheduleCommand(RunReminder(reminderId, deadline), "reminder-$reminderId", deadline)Here ReminderId extends Id<Reminder>, and deadline is the intended execution time. Schedule from a tracked
post-commit handler: the owner must already be committed in the schedule’s namespace. Do not schedule inside
replayed @Apply logic. A missing or deleted owner rejects planning.
For delayed work derived directly from Model state, see declarative Model deadlines. They combine intent changes and stale-delivery guards with the Model commit.
What deletion means
Section titled “What deletion means”Deleting an owner cancels its stored schedules, including when deletion cascades from another Model or the application is offline. Updating an owner, changing its status or moving it does not cancel work.
Use schedule reconciliation when a change of status or deadline should replace or cancel a schedule. Ownership handles deletion; reconciliation follows changing intent.
A payload can have several owners; deleting any one cancels the schedule. Null parents and
@Parent(deleteOnParentDeletion = false) do not create ownership. The payload itself does not need @Model.
Explicit selection, replacements and retries
Section titled “Explicit selection, replacements and retries”When ownership cannot be declared on the payload, select it on Schedule :
var schedule = new Schedule(new RunReminder(reminderId, deadline), "reminder-" + reminderId, deadline);Fluxzero.get().messageScheduler().scheduleCommand(schedule.withParents(reminderId));val schedule = Schedule(RunReminder(reminderId, deadline), "reminder-$reminderId", deadline)Fluxzero.get().messageScheduler().scheduleCommand(schedule.withParents(reminderId))withParents(...) replaces the payload’s declarations. withParents() deliberately makes the schedule unowned.
Prefer typed IDs; plain strings must contain the full canonical Model ID. Explicit IDs are placed in metadata, so
use the annotated-payload form for parent values that must remain protected.
Replacing work or retrying it
Section titled “Replacing work or retrying it”An accepted replacement with the same schedule ID replaces its ownership too. An ignored ifAbsent request does not.
Automatic retries and periodic continuations keep the original parent’s lifetime: deleting and recreating a Model
with the same ID does not revive its old work. Use explicit parents when deliberately starting work for a new lifetime.
See Schedule for rescheduling and replacement details.
Observe and test
Section titled “Observe and test”Test the active stored schedules after deletion, rather than only schedules dispatched during the test’s action:
fixture.givenCommands(new CreateReminder(reminderId)) .given(fc -> fc.messageScheduler().scheduleCommand( new Schedule(new RunReminder(reminderId, deadline), "reminder", deadline))) .whenCommand(new DeleteReminder(reminderId)) .expectOnlyActiveScheduledCommands() .expectNoErrors();In this example, CreateReminder creates the owner and DeleteReminder deletes it with an @Apply returning null.
Also test that a stale, already delivered RunReminder does no work. Local cancellation completes deterministically
after the Model commit; tests against a remote Runtime must await eventual cancellation.
ScheduleAutoCancelled reports removal from stored scheduling. It is a best-effort metric, not proof that an
already delivered command could not execute. See metrics.
Ownership requires a supporting scheduling service. Older or custom clients that lack support reject owned schedules.
© 2026 Fluxzero