Alarms and Conditions (OPC UA Part 9)

July 16, 2026 · View on GitHub

This guide describes the OPC UA Part 9 Alarms & Conditions support in this stack: how to build alarm-aware servers, how to consume alarm events on the client, and how the new helpers (latched alarms, alarm groups, suppression engine, alarm metrics, streaming alarm records) fit together.

For the formal model, see OPC UA Part 9 — Alarms and Conditions.

Quick reference

ConcernServer entry pointClient entry point
Create an alarmnew AlarmConditionState(telemetry, parent); alarm.Create(...)n/a
Acknowledge / ConfirmAcknowledgeableConditionState.OnAcknowledgeCalled (auto-wired)AlarmClient.AcknowledgeAsync / ConfirmAsync
SilenceAlarmConditionState.SetSilenceStateAlarmClient.SilenceAsync
Suppress / UnsuppressSetSuppressedStateAlarmClient.SuppressAsync / UnsuppressAsync
Out-of-serviceSetOutOfServiceStateAlarmClient.RemoveFromServiceAsync / PlaceInServiceAsync
Latched + ResetSetLatchedState + auto from SetActiveStateAlarmClient.ResetAsync
Shelve / UnshelveSetShelvingStateAlarmClient.TimedShelveAsync / OneShotShelveAsync / UnshelveAsync
Re-alarmProcessReAlarmn/a (server timer)
Alarm groupsAlarmGroup, AlarmSuppressionEngineAlarmClient.GetGroupMembershipsAsync
Alarm rateAlarmRateTrackerread AlarmMetricsType attributes
Refresh stateServer.ConditionRefresh (server-driven)AlarmClient.ConditionRefreshAsync / ConditionRefresh2Async
Stream alarm eventsn/aIStreamingSubscription.SubscribeAlarmsAsync
Decode raw event fieldsn/aEventRecordDecoderRegistry.Default.Decode
Build an event filtern/a{Type}Record.EventFilters.Build(registry?)

AlarmClient is obtained from any ISession and a telemetry context; internally it delegates every Part 9 method call to the matching source-generated *TypeClient proxy (so there is exactly one place that knows each method NodeId — the generator):

AlarmClient alarms = session.GetAlarmClient(telemetry);

For dependency-injected hosts use the builder.AddAlarms() extension and resolve AlarmClientFactory (which threads the host's telemetry into the client for you):

var factory = sp.GetRequiredService<AlarmClientFactory>();
AlarmClient alarms = factory.Create(session);

If you already have a typed handle to a specific condition node you can also construct a proxy directly — AlarmClient is a convenience façade over these:

await new AlarmConditionTypeClient(session, conditionId, telemetry)
    .AcknowledgeAsync(eventId, comment);

Server side

Creating an alarm

AlarmConditionState is the central server-side state type for all Part 9 alarms. It is source-generated from the standard NodeSet and extended with hand-written behavior. The behavior partial file (src/Opc.Ua.Core.Types/State/AlarmConditionState.Methods.cs) wires every Part 9 method handler during OnAfterCreate, so as soon as you populate the optional state nodes the corresponding methods are callable.

var alarm = new AlarmConditionState(telemetry, parent);
alarm.Create(
    context,
    nodeId: NodeId.Null,         // server-assigned
    browseName: new QualifiedName("MyAlarm"),
    displayName: null,
    assignNodeIds: true);

alarm.SetEnableState(context, enabled: true);
alarm.SetSeverity(context, EventSeverity.High);
alarm.SetActiveState(context, active: true);
alarm.ReportEvent(context, alarm);

The optional state nodes are created the way you would expect on a generated AlarmConditionState: assign to alarm.SilenceState, alarm.OutOfServiceState, alarm.LatchedState, etc. before calling Create. The quickstart reference server creates these via AlarmConditionTypeHolder.Initializesamples/Quickstarts.Servers/Alarms/AlarmHolders/.

Driving state from your process

All state transitions go through typed setters that:

  • update the TwoStateVariableState value + Id
  • stamp TransitionTime
  • recompute EffectiveDisplayName and the composite SuppressedOrShelved flag
  • clear ChangeMasks so the next publish cycle sees the new state
SetterNotes
SetEnableState(context, enabled)Inherited from ConditionState. Disabling clears Retain per Part 9 §5.5.2.
SetSeverity(context, severity)Records LastSeverity before updating.
SetActiveState(context, active)On true: if LatchedState is present, sets it true; if SilenceState is present and silenced, clears it. On false: if shelved as OneShotShelve, unshelves.
SetSuppressedState(context, suppressed)Updates SuppressedOrShelved taking OutOfServiceState and ShelvingState into account.
SetOutOfServiceState(context, outOfService)Sets SuppressedOrShelved true when out of service (Part 9 §5.8.2).
SetShelvingState(context, shelved, oneShot, shelvingTime)Drives the ShelvedStateMachineState, runs the unshelve timer, computes UnshelveTime.
SetLatchedState(context, latched)Direct latched-state setter; usually called automatically from SetActiveState.
SetSilenceState(context, silenced)Direct silence-state setter; usually called automatically on activation / re-alarm.

Latched alarms

If you populate alarm.LatchedState, the alarm becomes a latching alarm (Part 9 §4.8). The semantics:

  • SetActiveState(context, true) — also sets LatchedState = true.
  • SetActiveState(context, false)ActiveState reflects the real process state, but LatchedState stays true.
  • Reset (server-side method, auto-wired) — clears LatchedState. The Reset method validates all preconditions before accepting: enabled, not active, acknowledged, confirmed (if ConfirmedState is present). Any other state returns Bad_InvalidState.

Latched alarms are retained (Retain = true) for as long as LatchedState.Id is true, so a client refresh sees them.

Re-alarming

A re-alarm reminder fires when an alarm has been active and unacknowledged longer than ReAlarmTime. The state type provides a helper rather than an automatic timer (so the host owns scheduling and event generation):

// In your re-alarm scheduler (e.g. an external Timer)
if (alarm.IsReAlarmEnabled && alarm.ActiveState.Id.Value
    && alarm.AckedState?.Id.Value != true)
{
    alarm.ProcessReAlarm(context);
}

// On deactivation / acknowledge:
alarm.ResetReAlarmRepeatCount(context);

ProcessReAlarm:

  • clears AckedState (forces re-acknowledgement)
  • clears SilenceState (audible annunciation resumes)
  • increments ReAlarmRepeatCount
  • calls ReportStateChange so the new event is published

Audible alarms and silencing

If AudibleEnabled = true and AudibleSound is populated, the UpdateAudibleState helper takes care of clearing the silence state when the alarm activates (so the next activation is audible again):

ByteString sound = LoadWavFile();
alarm.UpdateAudibleState(context, active: true, soundData: sound);

SilenceAsync from the client (or the Silence method handler on the server) sets SilenceState.Id = true.

Suppression, out-of-service, shelving

All three states contribute to the SuppressedOrShelved boolean. The state-type setters keep that flag in sync — clearing one does not clear SuppressedOrShelved while the others are still active:

alarm.SetSuppressedState(context, true);      // SuppressedOrShelved = true
alarm.SetOutOfServiceState(context, true);    // SuppressedOrShelved stays true
alarm.SetSuppressedState(context, false);     // SuppressedOrShelved stays true
alarm.SetOutOfServiceState(context, false);   // SuppressedOrShelved = false

Alarm groups and first-in-group

src/Opc.Ua.Server/Alarms/AlarmGroup.cs wraps a generated AlarmGroupState and provides typed add/remove/enumerate:

var group = new AlarmGroup(motorAlarmGroupState);
group.AddMember(motorHighTempAlarm);
group.AddMember(motorLowOilAlarm);

foreach (NodeId id in group.GetMemberIds(context))
{
    // ...
}

AlarmSuppressionEngine centralizes both the AlarmSuppressionGroup pattern and the FirstInGroup pattern. Register on startup, call Evaluate from your simulation/process loop, and the engine routes suppression to the right alarm members:

using var engine = new AlarmSuppressionEngine();

engine.RegisterSuppressionGroup(
    suppressionGroup: motorShutdownGroup,
    suppressionSource: () => motorIsShutDown.Value,
    alarmMembers: new[] { motorHighTempAlarm, motorLowOilAlarm });

engine.RegisterFirstInGroupAlarm(
    firstAlarm: masterTripAlarm,
    group: tripGroup,
    otherMembers: dependentTripAlarms);

// In your periodic update:
engine.Evaluate(context);

// On master trip activation:
engine.OnFirstInGroupActiveChanged(context, masterTripAlarm, tripGroup,
    firstActive: true);

The first Evaluate call always applies the current state, so clients see a coherent suppression state immediately after registration — there is no edge required.

Live demo: The samples/Quickstarts.Servers/Alarms/ AlarmNodeManager.cs reference implementation wires this up end-to-end. It exposes a /Alarms/AnalogGroup (AlarmGroupType) containing every analog-source alarm and a writable /Alarms/MaintenanceMode boolean. Writing true to MaintenanceMode runs AlarmSuppressionEngine.Evaluate(...) and suppresses every group member; writing false clears suppression on the next evaluation. The reference server's node manager itself derives from AsyncCustomNodeManager, so the same file is a worked example of porting a Part 9 demo to the modern async base class.

Alarm metrics (rate tracking)

AlarmRateTracker records activations into a sliding window and exposes CurrentAlarmRate / MaximumAlarmRate suitable for surfacing through an AlarmMetricsType instance:

var tracker = new AlarmRateTracker(TimeSpan.FromMinutes(1));

alarm.OnSilenceRequested = (ctx, a) =>
{
    tracker.RecordActivation();
    return ServiceResult.Good;
};

// Periodically push to AlarmMetrics:
metrics.CurrentAlarmRate.Value = tracker.CurrentAlarmRate;
metrics.MaximumAlarmRate.Value = tracker.MaximumAlarmRate;

Vetoing alarm operations

Each Part 9 alarm method has an optional delegate that runs before the default state transition. Returning a Bad status from the delegate aborts the operation; returning Good (or null) lets the default behavior run:

DelegateTriggered by
alarm.OnSilenceRequestedSilence method
alarm.OnSuppressRequested(suppressing: bool)Suppress / Unsuppress
alarm.OnOutOfServiceRequested(outOfService: bool)RemoveFromService / PlaceInService
alarm.OnResetRequestedReset (latched alarms)
alarm.OnShelveOneShotShelve / TimedShelve / Unshelve (existing)
alarm.OnResetRequested = (ctx, a) =>
{
    if (!hardwareDiagnosticPassed)
    {
        return new ServiceResult(StatusCodes.BadUserAccessDenied,
            "Hardware diagnostic must pass before reset.");
    }
    return ServiceResult.Good;
};

Audit events

Every Part 9 alarm method generates the spec-mandated audit event type automatically. You do not have to call ReportEvent for the audit event — it happens inside the method handler when AreEventsMonitored is true:

MethodAudit event type
SilenceAuditConditionSilenceEventType
Suppress / Unsuppress / *2AuditConditionSuppressionEventType
RemoveFromService / PlaceInService / *2AuditConditionOutOfServiceEventType
Reset / Reset2AuditConditionResetEventType
Existing: Acknowledge / ConfirmAuditConditionAcknowledgeEventType / AuditConditionConfirmEventType
Existing: shelvingAuditConditionShelvingEventType

Client side

AlarmClient — typed operations

AlarmClient is the strongly-typed client API for Part 9 methods. Each method delegates to the matching source-generated *TypeClient proxy (ConditionTypeClient, AcknowledgeableConditionTypeClient, AlarmConditionTypeClient, DialogConditionTypeClient, ShelvedStateMachineTypeClient), passing the caller-supplied conditionId as the proxy's ObjectId. This honours the Part 9 §5.5.4 idiom (ConditionId is acceptable as ObjectId) and keeps exactly one source of truth — the generated proxy — for every method NodeId and argument shape.

AlarmClient alarms = session.GetAlarmClient(telemetry);

// ConditionType methods
await alarms.EnableAsync(conditionId);
await alarms.DisableAsync(conditionId);
await alarms.AddCommentAsync(conditionId, eventId,
    new LocalizedText("en", "Looks like flow sensor drift"));
await alarms.ConditionRefreshAsync(subscriptionId);
await alarms.ConditionRefresh2Async(subscriptionId, monitoredItemId);

// AcknowledgeableConditionType
await alarms.AcknowledgeAsync(conditionId, eventId,
    new LocalizedText("en", "Operator review complete"));
await alarms.ConfirmAsync(conditionId, eventId,
    new LocalizedText("en", "Maintenance verified"));

// AlarmConditionType
await alarms.SilenceAsync(conditionId);
await alarms.SuppressAsync(conditionId,
    comment: new LocalizedText("en", "Routine maintenance"));
await alarms.UnsuppressAsync(conditionId);
await alarms.RemoveFromServiceAsync(conditionId);
await alarms.PlaceInServiceAsync(conditionId);
await alarms.ResetAsync(conditionId);                            // latched-alarm reset
await alarms.TimedShelveAsync(conditionId, shelvingTime: 30000); // 30s
await alarms.OneShotShelveAsync(conditionId);
await alarms.UnshelveAsync(conditionId);

ArrayOf<NodeId> groups = await alarms.GetGroupMembershipsAsync(conditionId);

The *Async(... comment ...) overloads automatically pick the spec-defined *2 method when a non-empty comment is supplied (Suppress2, Unsuppress2, RemoveFromService2, PlaceInService2, Reset2).

Subscribing to alarms with IAsyncEnumerable

Alarm events flow through the streaming subscription API. The AlarmStreamExtensions.SubscribeAlarmsAsync extension returns strongly-typed records:

ManagedSession session = ...;
IStreamingSubscription streaming = session.DefaultStreaming;

await foreach (ConditionTypeRecord record in streaming
    .SubscribeAlarmsAsync(notifierId: ObjectIds.Server, ct: ct)
    .ConfigureAwait(false))
{
    switch (record)
    {
        case ExclusiveLimitAlarmTypeRecord limit:
            Console.WriteLine($"{limit.SourceName} limit-state-id={limit.LimitState}");
            break;
        case AlarmConditionTypeRecord alarm when alarm.ActiveStateId == true:
            await alarms.AcknowledgeAsync(alarm.ConditionId,
                alarm.EventId,
                new LocalizedText("en", "Auto-ack")).ConfigureAwait(false);
            break;
        case DialogConditionTypeRecord dialog:
            // Pick a response index from dialog.ResponseOptionSet
            await alarms.RespondAsync(dialog.ConditionId,
                selectedResponse: 0).ConfigureAwait(false);
            break;
    }
}

State-machine waits compose naturally with TakeUntilAsync / WithTimeoutAsync:

// Wait for myAlarm to clear, or 5 minutes — whichever comes first.
await streaming.SubscribeAlarmsAsync(ObjectIds.Server)
    .TakeUntilAsync(r =>
        r is AlarmConditionTypeRecord a && a.ConditionId == myAlarmId &&
        a.ActiveStateId == false)
    .WithTimeoutAsync(TimeSpan.FromMinutes(5))
    .LastAsync(ct);

Typed alarm records

Alarm and condition records are source-generated by the Opc.Ua.SourceGeneration analyzer (see the EventRecordGenerator). For every ObjectType whose base-type chain ends at BaseEventType, the generator emits a partial record {Type}Record deriving from the record of its parent type, exposing one init-only property per declared field. The standard NodeSet produces the following hierarchy (abridged — only the most commonly observed types are shown):

EventRecord                              (anchor; hand-written)
└── BaseEventTypeRecord                  (i=2041)
    └── ConditionTypeRecord              (i=2782)
        ├── DialogConditionTypeRecord    (i=2830)
        └── AcknowledgeableConditionTypeRecord
            └── AlarmConditionTypeRecord
                ├── LimitAlarmTypeRecord
                │   ├── ExclusiveLimitAlarmTypeRecord
                │   └── NonExclusiveLimitAlarmTypeRecord
                ├── DiscreteAlarmTypeRecord
                │   └── OffNormalAlarmTypeRecord
                │       └── CertificateExpirationAlarmTypeRecord
                └── DiscrepancyAlarmTypeRecord

Vendor models that derive from any of these types automatically get their own *TypeRecord deriving from the closest standard ancestor — no checked-in code, no manual class definitions. Add a hand-written partial record VibrationAlarmTypeRecord in your project to extend the generated declaration with computed properties or custom helpers.

The decoder upgrades the record type based on which fields are populated in the event. A simple switch on the record type gives you the right field set:

EventRecord? record = EventRecordDecoderRegistry.Default.Decode(eventFields);

if (record is CertificateExpirationAlarmTypeRecord cert)
{
    Console.WriteLine($"Cert {cert.CertificateType} expires {cert.ExpirationDate}");
}

The shared ConditionTypeRecord.ConditionId property is a hand-written alias for SourceNode — Part 9 of the OPC UA specification defines the "ConditionId" as the NodeId of the condition object that fired the event, which is reported through the SourceNode event field.

Source-generated decoders + EventRecordDecoderRegistry

Every record emitted by the EventRecordGenerator also exposes a nested static class Decoder with a positional StandardFields table and a Decode(IReadOnlyList<Variant>) method that populates own + inherited init-only properties. A per-file Register{ModelPrefix}Decoders(this EventRecordDecoderRegistry) extension registers every emitted decoder with a caller-supplied registry.

The process-wide EventRecordDecoderRegistry.Default ships with the standard UA model pre-registered. It routes by the event's EventType field and walks the OPC UA event-type hierarchy through an optional SuperTypeResolver when the exact type is not registered. Vendor models register their generated extensions via Register{Prefix}Decoders or CreateChildScope().Register{Prefix}Decoders() for test isolation.

// EventType-keyed dispatch backed by source-generated decoders:
EventRecord? rec = EventRecordDecoderRegistry.Default.Decode(eventFields);

// Vendor scenario — register extra decoders on an app-scoped child:
var app = EventRecordDecoderRegistry.Default
    .CreateChildScope()
    .RegisterMyVendorDecoders();
EventRecord? vendorRec = app.Decode(eventFields);

Per-record event filters

Every generated {Type}Record exposes a nested static class EventFilters alongside its Decoder block. The Build(registry?) factory produces an EventFilter whose where clause restricts events to OfType({recordTypeId}) and whose select clauses come from the supplied registry's composed StandardFields (defaults to EventRecordDecoderRegistry.Default). The returned filter pairs cleanly with EventRecordDecoderRegistry.Decode — the registry remaps the composed positions to each decoder's own layout before invoking it, so vendor models extend transparently.

// Filter for alarm events:
EventFilter filter = AlarmConditionTypeRecord.EventFilters.Build();

// Or for any condition:
EventFilter f2 = ConditionTypeRecord.EventFilters.Build();

// Or for dialog events:
EventFilter f3 = DialogConditionTypeRecord.EventFilters.Build();

// Or for a specific subtype:
EventFilter f4 = CertificateExpirationAlarmTypeRecord.EventFilters.Build();

// Vendor scenario — pass a child registry so the filter superset
// includes the vendor model's fields:
var app = EventRecordDecoderRegistry.Default
    .CreateChildScope()
    .RegisterMyVendorDecoders();
EventFilter vendorFilter = VibrationAlarmTypeRecord.EventFilters.Build(app);

The same registry should be passed to Subscribe*Async (through the registry: parameter) so the streaming side decodes through the registry that built the filter.

Dialog conditions

A DialogConditionType event arrives as a DialogConditionTypeRecord. The Respond and Respond2 methods on IDialogConditionOperations close out the dialog. The decoded record exposes the prompt and available response option set so the caller can pick an index:

await foreach (DialogConditionTypeRecord dialog in streaming.SubscribeDialogsAsync(notifierId))
{
    Console.WriteLine($"Prompt: {dialog.Prompt}");
    LocalizedText[] options = dialog.ResponseOptionSet ?? Array.Empty<LocalizedText>();
    Console.WriteLine($"Options: {string.Join(", ", options)}");

    // Pick whichever option matches your scenario.
    int selectedIndex = 0;
    await alarms.Respond2Async(dialog.ConditionId, selectedIndex,
        new LocalizedText("en", "Approved by operator-1")).ConfigureAwait(false);
}

The OkResponse / CancelResponse / DefaultResponse properties on the server-side DialogConditionType (Part 9 §5.6.2) carry their canonical indices for clients to read separately via Read service if the application needs them; they are not surfaced in the standard DialogConditionTypeRecord (which only carries the dialog prompt + active state).

ConditionRefresh

Both Part 9 refresh methods are available:

// Refresh all conditions for the subscription
await alarms.ConditionRefreshAsync(subscriptionId);

// Refresh just one monitored item's conditions (Part 9 §5.5.8)
await alarms.ConditionRefresh2Async(subscriptionId, monitoredItemId);

The classic ISubscription.ConditionRefreshAsync on the streaming subscription is also available — they are equivalent when the AlarmClient and the IStreamingSubscription are bound to the same session.

Reference

  • OPC UA Part 9 — Alarms and Conditions
  • IEC 62682 — Management of alarm systems for the process industries
  • ISA 18.2 — Management of Alarm Systems for the Process Industries
  • Subscriptions and Monitored Items Service SetIStreamingSubscription and the V2 subscription engine
  • State Machines — generic Part 16 state-machine API used by AlarmClient.GetShelvingStateAsync / ObserveShelvingTransitionsAsync
  • Model Change Tracking — client cache invalidation on address-space changes
  • Source: src/Opc.Ua.Server/Alarms/, src/Opc.Ua.Client/Alarms/, src/Opc.Ua.Core.Types/State/AlarmConditionState.Methods.cs
  • Reference client sample: samples/ConsoleReferenceClient/AlarmClientSample.cs
  • Conformance tests: tests/Opc.Ua.History.Tests/AlarmsAndConditions*.cs