Repository: JasperFx/wolverine (2026-06-23T21:29:03Z)

Issue:
Add user-defined service Tags to WolverineOptions, surfaced on ServiceCapabilities
Add a user-defined, free-form **service-level tags** collection so operators can label a service and have those labels flow to monitoring tools.

### Wolverine work
- Add a tags collection to `WolverineOptions` (near `ServiceName`, `src/Wolverine/WolverineOptions.cs`) — e.g. `IList<string> Tags { get; }` or a `Tag(string)` builder. Free-form strings; operator owns any `key:value` convention.
- Add a `Tags` property to `ServiceCapabilities` (`src/Wolverine/Configuration/Capabilities/ServiceCapabilities.cs`) and populate it from `runtime.Options.Tags` in `ServiceCapabilities.ReadFrom(...)`. Keep it binary-safe (init/property add, consistent with the descriptor conventions).

### Why
CritterWatch will consume `ServiceCapabilities.Tags` to let users filter related systems on its `/services` page by their own labels. Distinct from existing *per-endpoint* tag concepts — this is service-level.

Consumer side tracked in CritterWatch (linked below).



Discussion:

Patch:
diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md
index 87e007378..c0c93db07 100644
--- a/docs/guide/configuration.md
+++ b/docs/guide/configuration.md
@@ -194,3 +194,26 @@ host.Services.GetRequiredService<IWolverineRuntime>()
 ```
 <sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Testing/CoreTests/Configuration/using_configure_wolverine.cs#L14-L39' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_using_configure_wolverine' title='Start of snippet'>anchor</a></sup>
 <!-- endSnippet -->
+
+## Service-Level Tags <Badge type="tip" text="6.15" />
+
+You can attach free-form, service-level **tags** to a Wolverine application through `WolverineOptions.Tags`. These are
+opaque strings — you own any `key:value` (or any other) convention — and they label the whole service rather than
+individual endpoints or messages:
+
+```cs
+builder.Services.AddWolverine(opts =>
+{
+    opts.ServiceName = "Orders";
+
+    // Free-form, operator-defined labels for this service
+    opts.Tags.Add("team:fulfillment");
+    opts.Tags.Add("tier:critical");
+    opts.Tags.Add("domain:orders");
+});
+```
+
+The tags are surfaced on `ServiceCapabilities.Tags`, the diagnostic snapshot Wolverine exposes for monitoring tools.
+[CritterWatch](https://github.com/JasperFx/CritterWatch) consumes them to let you group and filter related services on
+its dashboard by your own labels. They are distinct from any per-endpoint tagging — this is a single, service-wide set
+of labels.
diff --git a/src/Wolverine/Configuration/Capabilities/ServiceCapabilities.cs b/src/Wolverine/Configuration/Capabilities/ServiceCapabilities.cs
index 081fb217b..e6b89a5d2 100644
--- a/src/Wolverine/Configuration/Capabilities/ServiceCapabilities.cs
+++ b/src/Wolverine/Configuration/Capabilities/ServiceCapabilities.cs
@@ -32,6 +32,12 @@ public ServiceCapabilities(WolverineOptions options)
         DurabilitySettings = options.Durability.ToDescription();
 
         AddValue(nameof(options.ServiceName), options.ServiceName);
+
+        // User-defined service-level tags (GH-3240). Free-form strings surfaced on the inherited
+        // OptionsDescription.Tags (string[]); consumed by CritterWatch to let users filter related services by
+        // their own labels. Distinct from per-endpoint tag concepts — this is service-level.
+        Tags = options.Tags.ToArray();
+
         AddValue(nameof(options.DefaultExecutionTimeout), options.DefaultExecutionTimeout);
         AddValue(nameof(options.DefaultRemoteInvocationTimeout), options.DefaultRemoteInvocationTimeout);
         AddValue(nameof(options.DisableAllExternalListeners), options.DisableAllExternalListeners);
diff --git a/src/Wolverine/WolverineOptions.cs b/src/Wolverine/WolverineOptions.cs
index 582d5f9bd..2fd66e2ab 100644
--- a/src/Wolverine/WolverineOptions.cs
+++ b/src/Wolverine/WolverineOptions.cs
@@ -541,6 +541,13 @@ public ResourceMigrationFailureMode ResourceMigrationFailureMode
     /// </summary>
     public string ServiceName { get; set; } = null!;
 
+    /// <summary>
+    ///     Free-form, user-defined service-level tags. These are surfaced on <c>ServiceCapabilities.Tags</c> and flow
+    ///     to monitoring tools (e.g. CritterWatch) so operators can label and filter related services by their own
+    ///     conventions. The operator owns any <c>key:value</c> convention — Wolverine treats these as opaque strings.
+    /// </summary>
+    public IList<string> Tags { get; } = new List<string>();
+
     /// <summary>
     ///     This should probably *only* be used in development or testing
     ///     to latch all outgoing message sending


Test patch:
diff --git a/src/Testing/CoreTests/Acceptance/service_tags_3240.cs b/src/Testing/CoreTests/Acceptance/service_tags_3240.cs
new file mode 100644
index 000000000..4a2cc6b13
--- /dev/null
+++ b/src/Testing/CoreTests/Acceptance/service_tags_3240.cs
@@ -0,0 +1,38 @@
+using Microsoft.Extensions.Hosting;
+using Shouldly;
+using Wolverine;
+using Wolverine.Configuration.Capabilities;
+using Wolverine.Tracking;
+using Xunit;
+
+namespace CoreTests.Acceptance;
+
+// GH-3240: user-defined service-level tags on WolverineOptions flow to ServiceCapabilities.Tags so CritterWatch
+// can filter related services by the operator's own labels.
+public class service_tags_3240
+{
+    [Fact]
+    public async Task user_tags_flow_to_service_capabilities()
+    {
+        using var host = await Host.CreateDefaultBuilder()
+            .UseWolverine(opts =>
+            {
+                opts.Tags.Add("team:payments");
+                opts.Tags.Add("tier:critical");
+            }).StartAsync();
+
+        var capabilities = await ServiceCapabilities.ReadFrom(host.GetRuntime(), null, CancellationToken.None);
+
+        capabilities.Tags.ShouldBe(["team:payments", "tier:critical"]);
+    }
+
+    [Fact]
+    public async Task tags_default_to_empty()
+    {
+        using var host = await Host.CreateDefaultBuilder().UseWolverine().StartAsync();
+
+        var capabilities = await ServiceCapabilities.ReadFrom(host.GetRuntime(), null, CancellationToken.None);
+
+        capabilities.Tags.ShouldBeEmpty();
+    }
+}