diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DataConnection/WriteTagBatchRequest.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DataConnection/WriteTagBatchRequest.cs
new file mode 100644
index 00000000..8c55f4d2
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/DataConnection/WriteTagBatchRequest.cs
@@ -0,0 +1,40 @@
+namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.DataConnection;
+
+///
+/// Request to write a SET of device tags through the DCL in ONE batch round-trip,
+/// optionally followed by a trigger-flag write. Composes the adapter's
+///
+/// (the batch) with a single WriteAsync (the trigger) so the script-facing
+/// Attributes.WriteBatchAndWaitAsync helper replaces N sequential per-attribute
+/// writes with one gateway call. The wait half is the EXISTING event-driven
+/// WaitForAttribute waiter — it is NOT part of this DCL message; failures are
+/// returned synchronously to the calling Instance Actor.
+///
+/// Per-write correlation id; echoed on the response.
+/// The data connection that owns every tag in the batch (and the trigger).
+/// Device tag path → value to write in the single batch round-trip.
+/// Optional device tag path of a flag written AFTER the batch succeeds; null to skip.
+/// Value to write to (ignored when it is null/empty).
+/// When the request was issued (UTC).
+public record WriteTagBatchRequest(
+ string CorrelationId,
+ string ConnectionName,
+ IReadOnlyDictionary Values, // device tagPath -> value
+ string? TriggerTagPath, // optional flag written AFTER the batch
+ object? TriggerValue,
+ DateTimeOffset Timestamp);
+
+///
+/// Response for a . is true only
+/// when the whole batch AND the optional trigger committed; otherwise
+/// describes the first failing leg.
+///
+/// Echoes the request's correlation id.
+/// True when the batch (and trigger, if any) all committed.
+/// Non-null on failure — the aggregated batch error, the trigger error, a timeout, or an adapter exception.
+/// When the response was produced (UTC).
+public record WriteTagBatchResponse(
+ string CorrelationId,
+ bool Success,
+ string? ErrorMessage,
+ DateTimeOffset Timestamp);
diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/WriteAttributeBatch.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/WriteAttributeBatch.cs
new file mode 100644
index 00000000..b836246b
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Messages/Instance/WriteAttributeBatch.cs
@@ -0,0 +1,43 @@
+namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Instance;
+
+///
+/// Request to write a SET of data-sourced attributes on one instance to the device in a
+/// single DCL batch round-trip, optionally followed by a trigger-flag attribute write.
+/// The Instance Actor resolves each attribute's data binding (connection + device tag
+/// path), decodes List values, enforces single-connection scope, and forwards one
+/// WriteTagBatchRequest to the DCL. This is the write half of the script-facing
+/// Attributes.WriteBatchAndWaitAsync helper; the wait half reuses the existing
+/// event-driven WaitForAttribute waiter.
+///
+///
+/// Site-local only. Values are carried codec-ENCODED (strings), so this message
+/// would serialize — but the helper composing it issues the wait via the in-process
+/// predicate-capable WaitForAttribute path, so the whole flow stays within one
+/// site node's actor system (script execution → Instance Actor → DCL).
+///
+///
+/// Per-write correlation id; echoed on the response.
+/// The instance whose attributes are written.
+/// Canonical (scope-resolved) attribute name → codec-encoded value.
+/// Optional canonical attribute name of a flag written AFTER the batch; null to skip.
+/// Codec-encoded value for .
+/// When the request was issued (UTC).
+public record WriteAttributeBatchRequest(
+ string CorrelationId,
+ string InstanceName,
+ IReadOnlyDictionary AttributeEncodedValues, // canonical attr name -> codec-encoded value
+ string? TriggerAttribute,
+ string? TriggerEncodedValue,
+ DateTimeOffset OccurredAtUtc);
+
+///
+/// Reply to a . is true only
+/// when the DCL batch (and trigger, if any) all committed.
+///
+/// Echoes the request's correlation id.
+/// True when the batch (and trigger, if any) all committed at the device.
+/// Non-null on failure — an unresolved/non-data-sourced attribute, a multi-connection batch, a list-decode failure, or the DCL error/timeout.
+public record WriteAttributeBatchResponse(
+ string CorrelationId,
+ bool Success,
+ string? ErrorMessage);
diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs
index 24b71a04..60ad161a 100644
--- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionActor.cs
@@ -245,6 +245,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
break;
case SubscribeTagsRequest:
case WriteTagRequest:
+ case WriteTagBatchRequest:
case UnsubscribeTagsRequest:
case SubscribeAlarmsRequest:
case UnsubscribeAlarmsRequest:
@@ -331,6 +332,9 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
case WriteTagRequest req:
HandleWrite(req);
break;
+ case WriteTagBatchRequest req:
+ HandleWriteBatch(req);
+ break;
case TagValueReceived tvr:
HandleTagValueReceived(tvr);
break;
@@ -457,6 +461,7 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
break;
case SubscribeTagsRequest:
case WriteTagRequest:
+ case WriteTagBatchRequest:
case SubscribeAlarmsRequest:
Stash.Stash();
break;
@@ -1059,6 +1064,67 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}).PipeTo(sender);
}
+ ///
+ /// Batch write counterpart of . Writes every value in
+ /// to the device in ONE adapter
+ /// WriteBatchAsync round-trip, then (if present) writes the trigger flag with
+ /// a single WriteAsync. Both legs share one
+ /// budget (DataConnectionLayer-005). Any failed value in the batch, a failed trigger,
+ /// the timeout, or an adapter exception is translated into a failed
+ /// returned synchronously to the caller — never a
+ /// dropped reply. NOTE: this deliberately composes the batch + trigger primitives and
+ /// uses the EXISTING event-driven WaitForAttribute waiter for the wait half; it does
+ /// NOT call the adapter's poll-based WriteBatchAndWaitAsync.
+ ///
+ private void HandleWriteBatch(WriteTagBatchRequest request)
+ {
+ _log.Debug("[{0}] Batch-writing {1} tag(s)", _connectionName, request.Values.Count);
+ var sender = Sender;
+ var cts = new CancellationTokenSource(_options.WriteTimeout);
+
+ async Task RunAsync()
+ {
+ try
+ {
+ var results = await _adapter.WriteBatchAsync(
+ new Dictionary(request.Values), cts.Token);
+ var failed = results.Values.Where(r => !r.Success).Select(r => r.ErrorMessage).ToList();
+ if (failed.Count > 0)
+ return new WriteTagBatchResponse(
+ request.CorrelationId, false,
+ "Batch write failed: " + string.Join("; ", failed), DateTimeOffset.UtcNow);
+
+ if (!string.IsNullOrEmpty(request.TriggerTagPath))
+ {
+ var tr = await _adapter.WriteAsync(request.TriggerTagPath, request.TriggerValue, cts.Token);
+ if (!tr.Success)
+ return new WriteTagBatchResponse(
+ request.CorrelationId, false,
+ "Trigger write failed: " + tr.ErrorMessage, DateTimeOffset.UtcNow);
+ }
+
+ return new WriteTagBatchResponse(request.CorrelationId, true, null, DateTimeOffset.UtcNow);
+ }
+ catch (OperationCanceledException)
+ {
+ return new WriteTagBatchResponse(
+ request.CorrelationId, false,
+ $"Write timeout after {_options.WriteTimeout.TotalSeconds:F0}s", DateTimeOffset.UtcNow);
+ }
+ catch (Exception ex)
+ {
+ return new WriteTagBatchResponse(
+ request.CorrelationId, false, ex.GetBaseException().Message, DateTimeOffset.UtcNow);
+ }
+ finally
+ {
+ cts.Dispose();
+ }
+ }
+
+ RunAsync().PipeTo(sender);
+ }
+
// ── OPC UA Tag Browser (interactive design-time query) ──
///
diff --git a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionManagerActor.cs b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionManagerActor.cs
index 5069c1b1..a46b020e 100644
--- a/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionManagerActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/Actors/DataConnectionManagerActor.cs
@@ -46,6 +46,7 @@ public class DataConnectionManagerActor : ReceiveActor
Receive(HandleRouteAlarms);
Receive(HandleRouteAlarms);
Receive(HandleRouteWrite);
+ Receive(HandleRouteWriteBatch);
Receive(HandleRemoveConnection);
Receive(HandleGetAllHealthReports);
Receive(HandleBrowse);
@@ -141,6 +142,26 @@ public class DataConnectionManagerActor : ReceiveActor
}
}
+ ///
+ /// Routes a to the child
+ /// that owns the named connection — the batch
+ /// counterpart of . The manager owns only the
+ /// unknown-connection failure (the same split as every other routed message);
+ /// the child resolves connected/not-connected and the per-write outcomes.
+ ///
+ private void HandleRouteWriteBatch(WriteTagBatchRequest request)
+ {
+ if (_connectionActors.TryGetValue(request.ConnectionName, out var actor))
+ actor.Forward(request);
+ else
+ {
+ _log.Warning("No connection actor for {0}", request.ConnectionName);
+ Sender.Tell(new WriteTagBatchResponse(
+ request.CorrelationId, false,
+ $"Unknown connection: {request.ConnectionName}", DateTimeOffset.UtcNow));
+ }
+ }
+
///
/// Routes a from the central UI's OPC UA
/// Tag Browser to the child that owns the
diff --git a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs
index 03390ae1..3e3911fc 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptCompileSurface.cs
@@ -193,6 +193,9 @@ public sealed class ScriptCompileSurface
/// Mirrors AttributeAccessor.WaitForAsync.
public Task WaitForAsync(string key, object? targetValue, TimeSpan timeout, bool requireGoodQuality = false) => throw new NotSupportedException(CompileOnly);
public Task WaitForAsync(string key, Func