diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianProvisioning.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianProvisioning.cs
new file mode 100644
index 00000000..a767cb5d
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianProvisioning.cs
@@ -0,0 +1,73 @@
+namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+///
+/// Server-side historian tag provisioning — ensures the historian knows about the tags the
+/// address space historizes before values are written. Registered alongside
+/// and invoked by the address-space applier when historized
+/// nodes are (re)applied.
+///
+///
+/// Provisioning is best-effort and non-blocking: an unreachable or erroring historian
+/// never fails an address-space apply. Implementations return a
+/// tally instead of throwing, so the applier can surface
+/// a count without taking the server down. Non-historizable types are skipped (counted in
+/// ), not failed.
+///
+public interface IHistorianProvisioning
+{
+ ///
+ /// Ensures the supplied historian tags exist (create-or-update). Never throws; a transport or
+ /// backend failure is reported via .
+ ///
+ /// The tags to ensure, with their driver data type and optional metadata.
+ /// A cancellation token for the operation.
+ /// A tally of how the requests were handled.
+ Task EnsureTagsAsync(
+ IReadOnlyList requests, CancellationToken ct);
+}
+
+///
+/// A single historian tag to ensure — the driver-agnostic shape the applier hands to
+/// . A backend maps
+/// onto its native tag type and skips types it cannot historize.
+///
+/// The full reference / tag name to ensure in the historian.
+/// The driver-agnostic data type, used to select the historian tag type.
+/// Optional engineering unit (e.g. degC); null when unknown.
+/// Optional human-readable description; null when unknown.
+public sealed record HistorianTagProvisionRequest(
+ string TagName,
+ DriverDataType DataType,
+ string? EngineeringUnit,
+ string? Description);
+
+///
+/// The tally returned by . The buckets
+/// partition the input: Requested == Ensured + Skipped + Failed.
+///
+/// Total tags submitted.
+/// Tags the historian acknowledged as created or already present.
+/// Tags whose data type is not historizable on the backend (never sent).
+/// Tags that were sent but the backend did not acknowledge (incl. a swallowed transport error).
+public sealed record HistorianProvisionResult(
+ int Requested,
+ int Ensured,
+ int Skipped,
+ int Failed);
+
+///
+/// No-op — the applier's safe default when no historian
+/// backend is registered. Every call returns an all-zero tally and never touches a backend.
+///
+public sealed class NullHistorianProvisioning : IHistorianProvisioning
+{
+ /// The shared singleton instance.
+ public static readonly NullHistorianProvisioning Instance = new();
+
+ private NullHistorianProvisioning() { }
+
+ ///
+ public Task EnsureTagsAsync(
+ IReadOnlyList requests, CancellationToken ct) =>
+ Task.FromResult(new HistorianProvisionResult(0, 0, 0, 0));
+}
diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayTagProvisioner.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayTagProvisioner.cs
new file mode 100644
index 00000000..ebba67f2
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayTagProvisioner.cs
@@ -0,0 +1,81 @@
+using Microsoft.Extensions.Logging;
+using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
+
+///
+/// backed by the HistorianGateway EnsureTags path.
+/// Non-historizable driver types are skipped (never built into a definition); the historizable
+/// ones are mapped via and batched into a single
+/// EnsureTags call.
+///
+///
+/// Non-blocking. A historian that is unreachable or errors must never fail an address-space
+/// apply, so the gateway call is wrapped in a catch-all: any exception counts the whole sent batch
+/// as and returns. The method never throws and never
+/// logs tag values, hostnames, or credentials.
+///
+public sealed class GatewayTagProvisioner : IHistorianProvisioning
+{
+ private readonly IHistorianGatewayClient _client;
+ private readonly ILogger _logger;
+
+ /// Creates the provisioner over a gateway client seam.
+ /// The gateway client used for the EnsureTags path.
+ /// Logger for skip/failure diagnostics (never logs tag values).
+ public GatewayTagProvisioner(IHistorianGatewayClient client, ILogger logger)
+ {
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ public async Task EnsureTagsAsync(
+ IReadOnlyList requests, CancellationToken ct)
+ {
+ ArgumentNullException.ThrowIfNull(requests);
+
+ var definitions = new List(requests.Count);
+ var skipped = 0;
+
+ foreach (var request in requests)
+ {
+ if (!HistorianTypeMapper.IsHistorizable(request.DataType))
+ {
+ skipped++;
+ // Log only the (non-sensitive) data type — never the tag name.
+ _logger.LogDebug(
+ "Skipping provisioning of a non-historizable tag of type {DataType}.", request.DataType);
+ continue;
+ }
+
+ definitions.Add(new HistorianTagDefinition
+ {
+ TagName = request.TagName,
+ DataType = HistorianTypeMapper.ToHistorianDataType(request.DataType),
+ // Proto string fields are non-nullable — coalesce absent metadata to empty.
+ EngineeringUnit = request.EngineeringUnit ?? string.Empty,
+ Description = request.Description ?? string.Empty,
+ });
+ }
+
+ try
+ {
+ var results = await _client.EnsureTagsAsync(definitions, ct).ConfigureAwait(false);
+ var ensured = results.Results.Count(r => r.Success);
+ var failed = Math.Max(0, definitions.Count - ensured);
+ return new HistorianProvisionResult(requests.Count, ensured, skipped, failed);
+ }
+ catch (Exception exception)
+ {
+ // Non-blocking: a failed EnsureTags never fails the apply. Count the whole sent batch as
+ // Failed and return; log only the failure category (no tag values).
+ _logger.LogWarning(
+ "EnsureTags failed for {Count} historian tag(s) ({Exception}); provisioning deferred.",
+ definitions.Count, exception.GetType().Name);
+ return new HistorianProvisionResult(requests.Count, Ensured: 0, Skipped: skipped, Failed: definitions.Count);
+ }
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs
new file mode 100644
index 00000000..acdecee9
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/GatewayTagProvisionerTests.cs
@@ -0,0 +1,114 @@
+using Microsoft.Extensions.Logging.Abstractions;
+using Xunit;
+using ZB.MOM.WW.HistorianGateway.Contracts.Grpc;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests;
+
+public sealed class GatewayTagProvisionerTests
+{
+ private static GatewayTagProvisioner Provisioner(FakeHistorianGatewayClient fake) =>
+ new(fake, NullLogger.Instance);
+
+ [Fact]
+ public async Task Ensures_numeric_tags_with_mapped_type()
+ {
+ var fake = new FakeHistorianGatewayClient { EnsureTagsResult = new TagOperationResults() };
+ var p = Provisioner(fake);
+ var reqs = new[]
+ {
+ new HistorianTagProvisionRequest("Pump1.Temp", DriverDataType.Float32, "degC", "Temp"),
+ new HistorianTagProvisionRequest("Pump1.Run", DriverDataType.Boolean, null, null),
+ };
+
+ var result = await p.EnsureTagsAsync(reqs, TestContext.Current.CancellationToken);
+
+ Assert.NotNull(fake.LastEnsureDefinitions);
+ Assert.Equal(2, fake.LastEnsureDefinitions!.Count);
+ Assert.Equal(HistorianDataType.Float, fake.LastEnsureDefinitions[0].DataType);
+ Assert.Equal(HistorianDataType.Int1, fake.LastEnsureDefinitions[1].DataType);
+ Assert.Equal(2, result.Requested);
+ Assert.Equal(0, result.Skipped);
+ }
+
+ [Fact]
+ public async Task Maps_metadata_and_coalesces_null_metadata_to_empty()
+ {
+ var fake = new FakeHistorianGatewayClient { EnsureTagsResult = new TagOperationResults() };
+ var p = Provisioner(fake);
+
+ await p.EnsureTagsAsync(
+ new[]
+ {
+ new HistorianTagProvisionRequest("Pump1.Temp", DriverDataType.Float32, "degC", "Temp"),
+ new HistorianTagProvisionRequest("Pump1.Run", DriverDataType.Boolean, null, null),
+ },
+ TestContext.Current.CancellationToken);
+
+ var defs = fake.LastEnsureDefinitions!;
+ Assert.Equal("Pump1.Temp", defs[0].TagName);
+ Assert.Equal("degC", defs[0].EngineeringUnit);
+ Assert.Equal("Temp", defs[0].Description);
+ // Proto string fields are non-nullable — null metadata must coalesce to empty.
+ Assert.Equal(string.Empty, defs[1].EngineeringUnit);
+ Assert.Equal(string.Empty, defs[1].Description);
+ }
+
+ [Fact]
+ public async Task Deferred_types_are_skipped_not_sent()
+ {
+ var fake = new FakeHistorianGatewayClient { EnsureTagsResult = new TagOperationResults() };
+ var p = Provisioner(fake);
+
+ var result = await p.EnsureTagsAsync(
+ new[] { new HistorianTagProvisionRequest("Pump1.Name", DriverDataType.String, null, null) },
+ TestContext.Current.CancellationToken);
+
+ Assert.Empty(fake.LastEnsureDefinitions!); // String is deferred → never built into a definition
+ Assert.Equal(1, result.Requested);
+ Assert.Equal(1, result.Skipped);
+ }
+
+ [Fact]
+ public async Task Gateway_failure_is_swallowed_and_counted_not_thrown()
+ {
+ var fake = new FakeHistorianGatewayClient { EnsureTagsThrows = new Exception("boom") };
+ var p = Provisioner(fake);
+
+ var result = await p.EnsureTagsAsync(
+ new[] { new HistorianTagProvisionRequest("Pump1.Temp", DriverDataType.Float32, null, null) },
+ TestContext.Current.CancellationToken);
+
+ Assert.Equal(1, result.Failed); // non-blocking: no throw
+ }
+
+ [Fact]
+ public async Task Ensured_count_reflects_successful_results()
+ {
+ var fake = new FakeHistorianGatewayClient
+ {
+ EnsureTagsResult = new TagOperationResults
+ {
+ Results =
+ {
+ new TagOperationResult { Name = "Pump1.Temp", Success = true },
+ new TagOperationResult { Name = "Pump1.Run", Success = false, Error = "x" },
+ },
+ },
+ };
+ var p = Provisioner(fake);
+
+ var result = await p.EnsureTagsAsync(
+ new[]
+ {
+ new HistorianTagProvisionRequest("Pump1.Temp", DriverDataType.Float32, null, null),
+ new HistorianTagProvisionRequest("Pump1.Run", DriverDataType.Boolean, null, null),
+ },
+ TestContext.Current.CancellationToken);
+
+ Assert.Equal(2, result.Requested);
+ Assert.Equal(1, result.Ensured);
+ Assert.Equal(0, result.Skipped);
+ Assert.Equal(1, result.Failed);
+ }
+}