diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmActor.cs
new file mode 100644
index 0000000..da1a849
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmActor.cs
@@ -0,0 +1,60 @@
+using Akka.Actor;
+using Akka.Event;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
+
+public enum ScriptedAlarmActorState { Inactive, Active, Acknowledged }
+
+///
+/// State machine wrapping a single scripted alarm. Transitions:
+/// Inactive → Active → Acknowledged → Inactive.
+///
+/// Engine wiring (compile alarm expression via AlarmConditionService, persist state to
+/// ScriptedAlarmState ConfigDb table on PreRestart, emit history rows to
+/// HistorianAdapter) is staged for follow-up F9. This skeleton owns the state machine
+/// so DriverHostActor can spawn it as a child.
+///
+public sealed class ScriptedAlarmActor : ReceiveActor
+{
+ public sealed record ConditionMet(string Reason);
+ public sealed record AcknowledgeAlarm(string Actor);
+ public sealed record ConditionCleared;
+ public sealed record StateChanged(string AlarmId, ScriptedAlarmActorState State, DateTime AtUtc);
+
+ private readonly string _alarmId;
+ private readonly ILoggingAdapter _log = Context.GetLogger();
+ private ScriptedAlarmActorState _state = ScriptedAlarmActorState.Inactive;
+
+ public static Props Props(string alarmId) =>
+ Akka.Actor.Props.Create(() => new ScriptedAlarmActor(alarmId));
+
+ public ScriptedAlarmActor(string alarmId)
+ {
+ _alarmId = alarmId;
+
+ Receive(msg =>
+ {
+ if (_state != ScriptedAlarmActorState.Inactive) return;
+ Transition(ScriptedAlarmActorState.Active);
+ });
+ Receive(msg =>
+ {
+ if (_state != ScriptedAlarmActorState.Active) return;
+ Transition(ScriptedAlarmActorState.Acknowledged);
+ });
+ Receive(_ =>
+ {
+ if (_state == ScriptedAlarmActorState.Inactive) return;
+ Transition(ScriptedAlarmActorState.Inactive);
+ });
+ }
+
+ private void Transition(ScriptedAlarmActorState next)
+ {
+ var prev = _state;
+ _state = next;
+ _log.Info("ScriptedAlarm {Id}: {From} → {To}", _alarmId, prev, next);
+ Context.Parent.Tell(new StateChanged(_alarmId, next, DateTime.UtcNow));
+ // F9: emit history row via HistorianAdapter; persist state to ScriptedAlarmState DB.
+ }
+}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/ScriptedAlarmActorTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/ScriptedAlarmActorTests.cs
new file mode 100644
index 0000000..a6721c9
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ScriptedAlarms/ScriptedAlarmActorTests.cs
@@ -0,0 +1,44 @@
+using Akka.Actor;
+using Akka.TestKit;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms;
+using ZB.MOM.WW.OtOpcUa.Runtime.Tests.Harness;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.ScriptedAlarms;
+
+public sealed class ScriptedAlarmActorTests : RuntimeActorTestBase
+{
+ [Fact]
+ public void Full_state_cycle_publishes_StateChanged_to_parent_at_each_transition()
+ {
+ var parent = CreateTestProbe();
+ // Wrap the alarm actor under our probe as parent so StateChanged lands on the probe.
+ var actor = parent.ChildActorOf(ScriptedAlarmActor.Props("alarm-1"));
+
+ actor.Tell(new ScriptedAlarmActor.ConditionMet("threshold"));
+ var t1 = parent.ExpectMsg();
+ t1.State.ShouldBe(ScriptedAlarmActorState.Active);
+
+ actor.Tell(new ScriptedAlarmActor.AcknowledgeAlarm("joe"));
+ var t2 = parent.ExpectMsg();
+ t2.State.ShouldBe(ScriptedAlarmActorState.Acknowledged);
+
+ actor.Tell(new ScriptedAlarmActor.ConditionCleared());
+ var t3 = parent.ExpectMsg();
+ t3.State.ShouldBe(ScriptedAlarmActorState.Inactive);
+ }
+
+ [Fact]
+ public void Duplicate_ConditionMet_in_Active_is_ignored()
+ {
+ var parent = CreateTestProbe();
+ var actor = parent.ChildActorOf(ScriptedAlarmActor.Props("alarm-1"));
+
+ actor.Tell(new ScriptedAlarmActor.ConditionMet("first"));
+ parent.ExpectMsg();
+
+ actor.Tell(new ScriptedAlarmActor.ConditionMet("second"));
+ parent.ExpectNoMsg(TimeSpan.FromMilliseconds(200));
+ }
+}