React to Model transitions with Graph history
Trigger an action when a value crosses a threshold, rather than every time it is reported.
A room reports its temperature. Publish RoomBecameCold only when it goes from at least 18 degrees to below 18.
Record the observation
Section titled “Record the observation”@Modelpublic record Room(@EntityId RoomId roomId, double temperature) {}
public final class RoomId extends Id<Room> { public RoomId(String id) { super(id); }}
public record ReportTemperature(RoomId roomId, double temperature) { @Apply Room apply(@Nullable Room room) { return new Room(roomId, temperature); }}
public record RoomBecameCold(RoomId roomId) {}@Modeldata class Room(@EntityId val roomId: RoomId, val temperature: Double)
class RoomId(id: String) : Id<Room>(id)
data class ReportTemperature(val roomId: RoomId, val temperature: Double) { @Apply fun apply(room: Room?) = Room(roomId, temperature)}
data class RoomBecameCold(val roomId: RoomId)Compare the two states
Section titled “Compare the two states”public class ColdRoomAlert { @HandleEvent void handle(ReportTemperature event, Graph<Room> room) { var previous = room.previous(); if (previous != null && !previous.isEmpty() && previous.get().temperature() >= 18 && room.get().temperature() < 18) { Fluxzero.publishEvent(new RoomBecameCold(event.roomId())); } }}class ColdRoomAlert { @HandleEvent fun handle(event: ReportTemperature, room: Graph<Room>) { val previous = room.previous()?.get() ?: return val current = room.get() ?: return if (previous.temperature >= 18 && current.temperature < 18) { Fluxzero.publishEvent(RoomBecameCold(event.roomId)) } }}The injected Graph belongs to this event. It compares the states around that observation even if newer readings have already arrived. The first reading establishes a baseline and does not trigger an alert.
Try a crossing and another low reading
Section titled “Try a crossing and another low reading”var roomId = new RoomId("living-room");TestFixture.create(new ColdRoomAlert()) .givenCommands(new ReportTemperature(roomId, 20)) .whenCommand(new ReportTemperature(roomId, 17)) .expectEvents(new RoomBecameCold(roomId)) .andThen() .whenCommand(new ReportTemperature(roomId, 16)) .expectNoEventsLike(RoomBecameCold.class);val roomId = RoomId("living-room")TestFixture.create(ColdRoomAlert()) .givenCommands(ReportTemperature(roomId, 20.0)) .whenCommand(ReportTemperature(roomId, 17.0)) .expectEvents(RoomBecameCold(roomId)) .andThen() .whenCommand(ReportTemperature(roomId, 16.0)) .expectNoEventsLike(RoomBecameCold::class.java)This avoids alerts for consecutive low readings. Re-delivery of the crossing event can still repeat the reaction; any external action should follow the API integration guide. See Temporal Graphs for historical navigation.
© 2026 Fluxzero