Skip to content

Tagging

Tags provide a flexible way to organize, categorize, and identify events and observers (reactors, reducers, projections, read models) within Chronicle. By applying the [Tag] or [Tags] attribute, you can assign one or more tags that describe the purpose, domain, or context of your artifacts.

Note: Both [Tag] and [Tags] attributes can be used interchangeably. Use whichever feels more natural, or mix them - all tags will be merged together.

Tags are strings that can be attached to:

  • Events - both statically (via attributes) and dynamically (when appending)
  • Reactors - for organizing and filtering reactive behaviors
  • Reducers - for categorizing state aggregation logic
  • Projections - for organizing view models and read models
  • Read Models - for categorizing data models

Tags are stored with event metadata and observer definitions, enabling powerful filtering, querying, and organizational capabilities.

Events can also carry named tags: a name paired with a value, such as import-batch = 4711. Use them for runtime identifiers you want to find events by later.

Events can be tagged in two ways: statically using attributes, or dynamically when appending events.

Apply the [Tag] or [Tags] attribute to your event types to assign static tags that will always be associated with that event type:

using Cratis.Chronicle;
using Cratis.Chronicle.Events;
[EventType]
[Tag("analytics", "user-action")]
public record TaggingUserLoggedIn(string UserId, DateTimeOffset LoggedInAt);
// [Tags] (plural) is equivalent to [Tag] — use whichever reads more naturally
[EventType]
[Tags("analytics", "user-action")]
public record TaggingUserLoggedInAlternate(string UserId, DateTimeOffset LoggedInAt);
// Mixing [Tag] and [Tags] on the same type merges all the tags
[EventType]
[Tag("security")]
[Tags("audit")]
public record TaggingUserPasswordChanged(string UserId, DateTimeOffset ChangedAt);

View C# snippet source on GitHub

When appending events, you can provide additional tags that will be merged with any static tags:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
public class TaggingUserLoginService(IEventLog eventLog)
{
public Task RecordLogin(EventSourceId eventSourceId) =>
// The event will end up with four tags: ["analytics", "user-action", "production", "critical"]
eventLog.Append(
eventSourceId,
new TaggingUserLoggedIn("user123", DateTimeOffset.UtcNow),
tags: ["production", "critical"]);
}

View C# snippet source on GitHub

In this example, the event will have four tags: ["analytics", "user-action", "production", "critical"].

All tags (both static and dynamic) are available on the EventContext when processing events:

using System.Linq;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reducers;
public record TaggingUserAnalytics(int LoginCount, int CriticalLoginCount);
public class TaggingUserAnalyticsReducer : IReducerFor<TaggingUserAnalytics>
{
public TaggingUserAnalytics LoggedIn(TaggingUserLoggedIn @event, TaggingUserAnalytics? current, EventContext context)
{
var analytics = current ?? new TaggingUserAnalytics(0, 0);
// Access tags from the event context
var isCritical = context.Tags.Any(tag => tag.Value == "critical");
return analytics with
{
LoginCount = analytics.LoginCount + 1,
CriticalLoginCount = analytics.CriticalLoginCount + (isCritical ? 1 : 0)
};
}
}

View C# snippet source on GitHub

A plain tag says what kind of event you have: "checkout", "audit". Sometimes you also need to record which import batch, workflow run, or checkout session produced an event, and find those events again later. Packing that into a string such as "import-batch:4711" gives you a tag you can’t query by name, and it breaks as soon as a value contains the separator. A named tag keeps the name and the value apart.

You supply named tags with AppendWithNamedTags (or AppendManyWithNamedTags for a batch). Unlike plain tags, there is no attribute that declares them on an event type.

using Cratis.Chronicle;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
[EventType]
[Tag("catalog")]
public record TaggingCatalogItemImported(string Sku, string Title);
public class TaggingCatalogImportService(IEventLog eventLog)
{
public Task<AppendResult> Import(EventSourceId itemId, string sku, string title, string batchId) =>
// Plain tags: ["catalog", "import"]
// Named tags: import-batch = <batchId>, source-system = erp
eventLog.AppendWithNamedTags(
itemId,
new TaggingCatalogItemImported(sku, title),
namedTags: [new NamedTag("import-batch", batchId), new NamedTag("source-system", "erp")],
tags: ["import"]);
}

View C# snippet source on GitHub

Named tags and plain tags are stored separately. The [Tag] attribute and the tags values above still produce plain tags, and a named tag never appears in the plain tag list. Appending with Tags covers batch appends and how per-event and call-level named tags combine.

  • Matching is exact. Names and values compare ordinally and case-sensitively, so Import-Batch and import-batch are two different names.
  • Values are opaque. Chronicle never parses a value. It can contain :, ., , or any other character, and it can be empty.
  • Names can’t be blank. Chronicle rejects a named tag whose name is empty or whitespace.
  • One name, several values. The same name with different values gives you separate tags. An exact duplicate of a name and value pair is kept once.

Named tags come back on the EventContext next to the plain tags:

using System.Linq;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;
public class TaggingCatalogImportTracker : IReactor
{
public Task Imported(TaggingCatalogItemImported @event, EventContext context)
{
// Names and values compare exactly (ordinal, case-sensitive).
// A name can carry more than one value, so read every match.
var batchIds = context.NamedTags
.Where(tag => tag.Name.Value == "import-batch")
.Select(tag => tag.Value)
.ToArray();
Console.WriteLine($"{@event.Sku} imported in batch(es) {string.Join(", ", batchIds)}");
return Task.CompletedTask;
}
}

View C# snippet source on GitHub

The Chronicle server’s event sequence service answers named-tag queries through two operations: QueryEventsWithNamedTags returns a page of events, and SequenceHistogramWithNamedTags returns event counts over time. Each criterion takes one of three forms:

CriterionMatches events that carry
Name onlyThe name, with any value
Name and a valueExactly that name and value
Name and a set of valuesThe name with any of those values

An event matches when it satisfies any one of the criteria. The named-tag criteria then combine with every other filter on the query: event source, event source type, event stream type, correlation ID, event types, plain tags, and the occurred date range. A named-tag query needs at least one criterion, and a criterion with an empty set of values is rejected. Neither case turns into a query that returns everything.

MongoDB, SQL (SQLite, PostgreSQL, and SQL Server), and in-memory storage support named-tag appends and queries. SQL stores them in a companion table named __cratis_named_tags in each event-store namespace database. That name is reserved for Chronicle. If a conflicting table already exists, every event-sequence operation in that namespace fails with NamedTagsTableCollision on first access (migration is lazy) and continues to fail until the conflicting table is renamed.

Observers (reactors, reducers, and projections) can be tagged to organize and categorize them:

using Cratis.Chronicle;
using Cratis.Chronicle.Reactors;
[Tag("Notifications")]
public class TaggingOrderConfirmationReactor : IReactor;

View C# snippet source on GitHub

You can use either [Tag] or [Tags] (plural) for convenience:

using Cratis.Chronicle;
using Cratis.Chronicle.Reactors;
[Tag("Notifications", "Customer", "Email")]
public class TaggingCustomerNotificationReactor : IReactor;
// [Tags] (plural) is equivalent — use whichever reads more naturally
[Tags("Notifications", "Customer", "Email")]
public class TaggingCustomerNotificationReactorAlternate : IReactor;

View C# snippet source on GitHub

using Cratis.Chronicle;
using Cratis.Chronicle.Reactors;
[Tag("Integration")]
[Tag("ExternalAPI")]
[Tag("Inventory")]
public class TaggingInventorySyncReactor : IReactor;

View C# snippet source on GitHub

You can mix both [Tag] and [Tags] attributes - all tags will be merged:

using Cratis.Chronicle;
using Cratis.Chronicle.Reactors;
[Tag("Notifications", "SMS")]
[Tags("Customer")]
public class TaggingSmsNotificationReactor : IReactor;
// Or mix single and multiple attributes the other way around
[Tag("Integration")]
[Tags("ExternalAPI", "Inventory")]
public class TaggingInventorySyncReactorMixed : IReactor;

View C# snippet source on GitHub

Read models and projections can also be tagged:

using Cratis.Chronicle;
[Tag("Reporting", "Analytics")]
public record TaggingConceptsSalesReport(decimal TotalSales, int OrderCount);

View C# snippet source on GitHub

  • Use meaningful names: Choose tag names that clearly describe the purpose or domain
  • Be consistent: Establish tag naming conventions across your organization
  • Don’t over-tag: Apply only relevant tags; too many can reduce their usefulness
  • Group related artifacts: Use tags to group events and observers that serve similar purposes
  • Consider hierarchies: Use dot notation for fixed, hierarchical categories (e.g., "customer.registration", "customer.profile")
  • Don’t encode runtime values in plain tags: A batch number, run ID, or session ID belongs in a named tag, not in a string such as "batch:4711" or "batch.4711"

Here are some common patterns for organizing tags:

using Cratis.Chronicle;
[Tag("Sales")]
[Tag("Inventory")]
[Tag("Customer")]
[Tag("Shipping")]
public record TaggingByDomainExample;

View C# snippet source on GitHub

using Cratis.Chronicle;
[Tag("Analytics")]
[Tag("Reporting")]
[Tag("Integration")]
[Tag("Alerting")]
[Tag("Monitoring")]
[Tag("Automation")]
public record TaggingByPurposeExample;

View C# snippet source on GitHub

using Cratis.Chronicle;
[Tag("Notifications")]
[Tag("ExternalAPI")]
[Tag("MessageQueue")]
[Tag("FileSystem")]
public record TaggingByIntegrationTypeExample;

View C# snippet source on GitHub

using Cratis.Chronicle;
[Tag("Email")]
[Tag("SMS")]
[Tag("Push")]
[Tag("Webhook")]
public record TaggingByCommunicationChannelExample;

View C# snippet source on GitHub

using Cratis.Chronicle;
[Tag("Customer")]
[Tag("Operations")]
[Tag("Finance")]
[Tag("Support")]
[Tag("Executive")]
public record TaggingByStakeholderExample;

View C# snippet source on GitHub

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
[EventType]
public record TaggingDynamicTagsEventOccurred(string Data);
public class TaggingDynamicTagsService(IEventLog eventLog)
{
public Task RecordProductionCritical(EventSourceId eventSourceId) =>
eventLog.Append(eventSourceId, new TaggingDynamicTagsEventOccurred("production issue"), tags: ["production", "critical"]);
public Task RecordDevelopmentTest(EventSourceId eventSourceId) =>
eventLog.Append(eventSourceId, new TaggingDynamicTagsEventOccurred("test run"), tags: ["development", "testing"]);
public Task RecordBatchMigration(EventSourceId eventSourceId) =>
eventLog.Append(eventSourceId, new TaggingDynamicTagsEventOccurred("batch migration"), tags: ["migration", "batch-process"]);
}

View C# snippet source on GitHub

Tags stored in the event store and observer definitions can be used for:

  • Filtering and searching for specific events or observers
  • Organizing artifacts in administrative interfaces
  • Generating documentation about your system
  • Managing deployments by tag
  • Monitoring and alerting based on tag groups
  • Controlling activation of observers by tag
  • Analyzing event patterns and flows
  • Creating tag-based subscriptions or filters

Note: The specific querying and filtering capabilities depend on your Chronicle setup and tooling.