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.
Overview
Section titled “Overview”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.
Tagging Events
Section titled “Tagging Events”Events can be tagged in two ways: statically using attributes, or dynamically when appending events.
Static Event Tags
Section titled “Static Event Tags”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);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { eventType, tag, tags } from '@cratis/chronicle';
@eventType()@tag('analytics', 'user-action')class TaggingUserLoggedIn { constructor(readonly userId: string, readonly loggedInAt: Date) {}}
// @tags() (plural) is equivalent to @tag() — use whichever reads more naturally@eventType()@tags('analytics', 'user-action')class TaggingUserLoggedInAlternate { constructor(readonly userId: string, readonly loggedInAt: Date) {}}
// Mixing @tag() and @tags() on the same type merges all the tags@eventType()@tag('security')@tags('audit')class TaggingUserPasswordChanged { constructor(readonly userId: string, readonly changedAt: Date) {}}Dynamic Event Tags
Section titled “Dynamic Event Tags”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"]);}import io.cratis.chronicle.eventSequences.AppendOptionsimport io.cratis.chronicle.eventSequences.IEventLogimport io.cratis.chronicle.events.EventType
@EventTypedata class TaggingUserLoggedIn(val userId: String)
class TaggingUserLoginService(private val eventLog: IEventLog) { suspend fun recordLogin(eventSourceId: String) { eventLog.append( eventSourceId, TaggingUserLoggedIn("user123"), AppendOptions(tags = listOf("production", "critical")) ) }}import io.cratis.chronicle.eventSequences.IEventLog;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.AppendOptionsBuilder;import io.cratis.chronicle.java.EventLogJavaBridge;
@EventTyperecord TaggingUserLoggedIn(String userId) {}
class TaggingUserLoginService { private final IEventLog eventLog;
TaggingUserLoginService(IEventLog eventLog) { this.eventLog = eventLog; }
void recordLogin(String eventSourceId) { EventLogJavaBridge.append( eventLog, eventSourceId, new TaggingUserLoggedIn("user123"), new AppendOptionsBuilder().tag("production").tag("critical").build()); }}defmodule MyApp.Events.TaggingUserLoggedIn do use Chronicle.Events.EventType, id: "tagging-user-logged-in"
defstruct [:user_id, :logged_in_at]end
defmodule MyApp.TaggingUserLoginService do alias MyApp.Events.TaggingUserLoggedIn
def record_login(event_source_id, user_id) do # Elixir doesn't support static tags on the event type, so this event ends up # with only the two dynamic tags: ["production", "critical"] Chronicle.append( event_source_id, %TaggingUserLoggedIn{user_id: user_id, logged_in_at: DateTime.utc_now()}, tags: ["production", "critical"] ) endendimport { IEventStore } from '@cratis/chronicle';
class TaggingUserLoginService { constructor(private readonly store: IEventStore) {}
// The event will end up with four tags: ['analytics', 'user-action', 'production', 'critical'] async recordLogin(eventSourceId: string): Promise<void> { await this.store.eventLog.append( eventSourceId, new TaggingUserLoggedIn('user123', new Date()), { tags: ['production', 'critical'] }); }}In this example, the event will have four tags: ["analytics", "user-action", "production", "critical"].
Tags in Event Context
Section titled “Tags in Event Context”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) }; }}import io.cratis.chronicle.events.EventContextimport io.cratis.chronicle.observation.Reducerimport io.cratis.chronicle.readModels.ReadModel
@ReadModeldata class TaggingUserAnalytics(val loginCount: Int = 0, val criticalLoginCount: Int = 0)
@Reducerclass TaggingUserAnalyticsReducer { fun loggedIn( event: TaggingUserLoggedIn, current: TaggingUserAnalytics?, context: EventContext ): TaggingUserAnalytics { val analytics = current ?: TaggingUserAnalytics()
// Tags the event was appended with are available on the context. val isCritical = context.tags.contains("critical")
return analytics.copy( loginCount = analytics.loginCount + 1, criticalLoginCount = analytics.criticalLoginCount + if (isCritical) 1 else 0 ) }}import io.cratis.chronicle.events.EventContext;import io.cratis.chronicle.observation.Reducer;import io.cratis.chronicle.readModels.ReadModel;
@ReadModelrecord TaggingUserAnalytics(int loginCount, int criticalLoginCount) { static TaggingUserAnalytics initial() { return new TaggingUserAnalytics(0, 0); }}
@Reducerclass TaggingUserAnalyticsReducer { TaggingUserAnalytics loggedIn(TaggingUserLoggedIn event, TaggingUserAnalytics current, EventContext context) { TaggingUserAnalytics analytics = current != null ? current : TaggingUserAnalytics.initial();
// Tags the event was appended with are available on the context. boolean isCritical = context.getTags().contains("critical");
return new TaggingUserAnalytics( analytics.loginCount() + 1, analytics.criticalLoginCount() + (isCritical ? 1 : 0)); }}Elixir does not support this workflow yet.import { EventContext, reducer } from '@cratis/chronicle';
class TaggingUserAnalytics { loginCount = 0; criticalLoginCount = 0;}
@reducer('', undefined, TaggingUserAnalytics)class TaggingUserAnalyticsReducer { taggingUserLoggedIn( event: TaggingUserLoggedIn, current: TaggingUserAnalytics | undefined, context: EventContext ): TaggingUserAnalytics { const analytics = current ?? new TaggingUserAnalytics();
// Access tags from the event context const isCritical = context.tags.some(tag => tag.value === 'critical');
return { loginCount: analytics.loginCount + 1, criticalLoginCount: analytics.criticalLoginCount + (isCritical ? 1 : 0) }; }}Tagging Observers
Section titled “Tagging Observers”Observers (reactors, reducers, and projections) can be tagged to organize and categorize them:
Single Tag
Section titled “Single Tag”using Cratis.Chronicle;using Cratis.Chronicle.Reactors;
[Tag("Notifications")]public class TaggingOrderConfirmationReactor : IReactor;import io.cratis.chronicle.observation.Reactorimport io.cratis.chronicle.observation.Tag
@Tag("Notifications")@Reactorclass TaggingOrderConfirmationReactorimport io.cratis.chronicle.observation.Reactor;import io.cratis.chronicle.observation.Tag;
@Tag("Notifications")@Reactorclass TaggingOrderConfirmationReactor {}Elixir does not support this workflow yet.import { reactor, tag } from '@cratis/chronicle';
@reactor()@tag('Notifications')class TaggingOrderConfirmationReactor {}Multiple Tags (Single Attribute)
Section titled “Multiple Tags (Single Attribute)”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;import io.cratis.chronicle.observation.Reactorimport io.cratis.chronicle.observation.Tag
@Tag("Notifications", "Customer", "Email")@Reactorclass TaggingCustomerNotificationReactorimport io.cratis.chronicle.observation.Reactor;import io.cratis.chronicle.observation.Tag;
@Tag({"Notifications", "Customer", "Email"})@Reactorclass TaggingCustomerNotificationReactor {}Elixir does not support this workflow yet.import { reactor, tag, tags } from '@cratis/chronicle';
@reactor()@tag('Notifications', 'Customer', 'Email')class TaggingCustomerNotificationReactor {}
// @tags() (plural) is equivalent — use whichever reads more naturally@reactor()@tags('Notifications', 'Customer', 'Email')class TaggingCustomerNotificationReactorAlternate {}Multiple Tags (Multiple Attributes)
Section titled “Multiple Tags (Multiple Attributes)”using Cratis.Chronicle;using Cratis.Chronicle.Reactors;
[Tag("Integration")][Tag("ExternalAPI")][Tag("Inventory")]public class TaggingInventorySyncReactor : IReactor;import io.cratis.chronicle.observation.Reactorimport io.cratis.chronicle.observation.Tag
@Tag("Integration")@Tag("ExternalAPI")@Tag("Inventory")@Reactorclass TaggingInventorySyncReactorimport io.cratis.chronicle.observation.Reactor;import io.cratis.chronicle.observation.Tag;
@Tag("Integration")@Tag("ExternalAPI")@Tag("Inventory")@Reactorclass TaggingInventorySyncReactor {}Elixir does not support this workflow yet.import { reactor, tag } from '@cratis/chronicle';
@reactor()@tag('Integration')@tag('ExternalAPI')@tag('Inventory')class TaggingInventorySyncReactor {}Mixed Approach
Section titled “Mixed Approach”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;import io.cratis.chronicle.observation.Reactorimport io.cratis.chronicle.observation.Tag
// Kotlin exposes only @Tag (repeatable) - combine a multi-value use with a stacked one however reads best@Tag("Notifications", "SMS")@Tag("Customer")@Reactorclass TaggingSmsNotificationReactorimport io.cratis.chronicle.observation.Reactor;import io.cratis.chronicle.observation.Tag;
// Java exposes only @Tag (repeatable) - combine a multi-value use with a stacked one however reads best@Tag({"Notifications", "SMS"})@Tag("Customer")@Reactorclass TaggingSmsNotificationReactor {}Elixir does not support this workflow yet.import { reactor, tag, tags } from '@cratis/chronicle';
@reactor()@tag('Notifications', 'SMS')@tags('Customer')class TaggingSmsNotificationReactor {}
// Or mix single and multiple attributes the other way around@reactor()@tag('Integration')@tags('ExternalAPI', 'Inventory')class TaggingInventorySyncReactorMixed {}Tagging Read Models
Section titled “Tagging Read Models”Read models and projections can also be tagged:
using Cratis.Chronicle;
[Tag("Reporting", "Analytics")]public record TaggingConceptsSalesReport(decimal TotalSales, int OrderCount);Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { tag } from '@cratis/chronicle';
@tag('Reporting', 'Analytics')class TaggingConceptsSalesReport { constructor(readonly totalSales: number, readonly orderCount: number) {}}Best Practices
Section titled “Best Practices”- 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 hierarchical tags (e.g.,
"customer.registration","customer.profile")
Common Tagging Patterns
Section titled “Common Tagging Patterns”Here are some common patterns for organizing tags:
By Domain
Section titled “By Domain”using Cratis.Chronicle;
[Tag("Sales")][Tag("Inventory")][Tag("Customer")][Tag("Shipping")]public record TaggingByDomainExample;import io.cratis.chronicle.observation.Reactorimport io.cratis.chronicle.observation.Tag
@Tag("Sales")@Tag("Inventory")@Tag("Customer")@Tag("Shipping")@Reactorclass TaggingByDomainExampleimport io.cratis.chronicle.observation.Reactor;import io.cratis.chronicle.observation.Tag;
@Tag("Sales")@Tag("Inventory")@Tag("Customer")@Tag("Shipping")@Reactorclass TaggingByDomainExample {}Elixir does not support this workflow yet.import { tag } from '@cratis/chronicle';
@tag('Sales')@tag('Inventory')@tag('Customer')@tag('Shipping')class TaggingByDomainExample {}By Purpose
Section titled “By Purpose”using Cratis.Chronicle;
[Tag("Analytics")][Tag("Reporting")][Tag("Integration")][Tag("Alerting")][Tag("Monitoring")][Tag("Automation")]public record TaggingByPurposeExample;import io.cratis.chronicle.observation.Reactorimport io.cratis.chronicle.observation.Tag
@Tag("Analytics")@Tag("Reporting")@Tag("Integration")@Tag("Alerting")@Tag("Monitoring")@Tag("Automation")@Reactorclass TaggingByPurposeExampleimport io.cratis.chronicle.observation.Reactor;import io.cratis.chronicle.observation.Tag;
@Tag("Analytics")@Tag("Reporting")@Tag("Integration")@Tag("Alerting")@Tag("Monitoring")@Tag("Automation")@Reactorclass TaggingByPurposeExample {}Elixir does not support this workflow yet.import { tag } from '@cratis/chronicle';
@tag('Analytics')@tag('Reporting')@tag('Integration')@tag('Alerting')@tag('Monitoring')@tag('Automation')class TaggingByPurposeExample {}By Integration Type
Section titled “By Integration Type”using Cratis.Chronicle;
[Tag("Notifications")][Tag("ExternalAPI")][Tag("MessageQueue")][Tag("FileSystem")]public record TaggingByIntegrationTypeExample;import io.cratis.chronicle.observation.Reactorimport io.cratis.chronicle.observation.Tag
@Tag("Notifications")@Tag("ExternalAPI")@Tag("MessageQueue")@Tag("FileSystem")@Reactorclass TaggingByIntegrationTypeExampleimport io.cratis.chronicle.observation.Reactor;import io.cratis.chronicle.observation.Tag;
@Tag("Notifications")@Tag("ExternalAPI")@Tag("MessageQueue")@Tag("FileSystem")@Reactorclass TaggingByIntegrationTypeExample {}Elixir does not support this workflow yet.import { tag } from '@cratis/chronicle';
@tag('Notifications')@tag('ExternalAPI')@tag('MessageQueue')@tag('FileSystem')class TaggingByIntegrationTypeExample {}By Communication Channel
Section titled “By Communication Channel”using Cratis.Chronicle;
[Tag("Email")][Tag("SMS")][Tag("Push")][Tag("Webhook")]public record TaggingByCommunicationChannelExample;import io.cratis.chronicle.observation.Reactorimport io.cratis.chronicle.observation.Tag
@Tag("Email")@Tag("SMS")@Tag("Push")@Tag("Webhook")@Reactorclass TaggingByCommunicationChannelExampleimport io.cratis.chronicle.observation.Reactor;import io.cratis.chronicle.observation.Tag;
@Tag("Email")@Tag("SMS")@Tag("Push")@Tag("Webhook")@Reactorclass TaggingByCommunicationChannelExample {}Elixir does not support this workflow yet.import { tag } from '@cratis/chronicle';
@tag('Email')@tag('SMS')@tag('Push')@tag('Webhook')class TaggingByCommunicationChannelExample {}By Stakeholder
Section titled “By Stakeholder”using Cratis.Chronicle;
[Tag("Customer")][Tag("Operations")][Tag("Finance")][Tag("Support")][Tag("Executive")]public record TaggingByStakeholderExample;import io.cratis.chronicle.observation.Reactorimport io.cratis.chronicle.observation.Tag
@Tag("Customer")@Tag("Operations")@Tag("Finance")@Tag("Support")@Tag("Executive")@Reactorclass TaggingByStakeholderExampleimport io.cratis.chronicle.observation.Reactor;import io.cratis.chronicle.observation.Tag;
@Tag("Customer")@Tag("Operations")@Tag("Finance")@Tag("Support")@Tag("Executive")@Reactorclass TaggingByStakeholderExample {}Elixir does not support this workflow yet.import { tag } from '@cratis/chronicle';
@tag('Customer')@tag('Operations')@tag('Finance')@tag('Support')@tag('Executive')class TaggingByStakeholderExample {}By Environment or Context
Section titled “By Environment or Context”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"]);}import io.cratis.chronicle.eventSequences.AppendOptionsimport io.cratis.chronicle.eventSequences.IEventLogimport io.cratis.chronicle.events.EventType
@EventTypedata class TaggingDynamicTagsEventOccurred(val data: String)
class TaggingDynamicTagsService(private val eventLog: IEventLog) { suspend fun recordProductionCritical(eventSourceId: String) = eventLog.append( eventSourceId, TaggingDynamicTagsEventOccurred("production issue"), AppendOptions(tags = listOf("production", "critical")) )
suspend fun recordDevelopmentTest(eventSourceId: String) = eventLog.append( eventSourceId, TaggingDynamicTagsEventOccurred("test run"), AppendOptions(tags = listOf("development", "testing")) )
suspend fun recordBatchMigration(eventSourceId: String) = eventLog.append( eventSourceId, TaggingDynamicTagsEventOccurred("batch migration"), AppendOptions(tags = listOf("migration", "batch-process")) )}import io.cratis.chronicle.eventSequences.IEventLog;import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.java.AppendOptionsBuilder;import io.cratis.chronicle.java.EventLogJavaBridge;
@EventTyperecord TaggingDynamicTagsEventOccurred(String data) {}
class TaggingDynamicTagsService { private final IEventLog eventLog;
TaggingDynamicTagsService(IEventLog eventLog) { this.eventLog = eventLog; }
void recordProductionCritical(String eventSourceId) { EventLogJavaBridge.append( eventLog, eventSourceId, new TaggingDynamicTagsEventOccurred("production issue"), new AppendOptionsBuilder().tag("production").tag("critical").build()); }
void recordDevelopmentTest(String eventSourceId) { EventLogJavaBridge.append( eventLog, eventSourceId, new TaggingDynamicTagsEventOccurred("test run"), new AppendOptionsBuilder().tag("development").tag("testing").build()); }
void recordBatchMigration(String eventSourceId) { EventLogJavaBridge.append( eventLog, eventSourceId, new TaggingDynamicTagsEventOccurred("batch migration"), new AppendOptionsBuilder().tag("migration").tag("batch-process").build()); }}defmodule MyApp.Events.TaggingDynamicPatternsEventOccurred do use Chronicle.Events.EventType, id: "tagging-dynamic-patterns-event-occurred"
defstruct [:data]end
defmodule MyApp.TaggingDynamicPatternsService do alias MyApp.Events.TaggingDynamicPatternsEventOccurred
def record_production_critical(event_source_id) do Chronicle.append( event_source_id, %TaggingDynamicPatternsEventOccurred{data: "production issue"}, tags: ["production", "critical"] ) end
def record_development_test(event_source_id) do Chronicle.append( event_source_id, %TaggingDynamicPatternsEventOccurred{data: "test run"}, tags: ["development", "testing"] ) end
def record_batch_migration(event_source_id) do Chronicle.append( event_source_id, %TaggingDynamicPatternsEventOccurred{data: "batch migration"}, tags: ["migration", "batch-process"] ) endendimport { eventType, IEventStore } from '@cratis/chronicle';
@eventType()class TaggingDynamicTagsEventOccurred { constructor(readonly data: string) {}}
class TaggingDynamicTagsService { constructor(private readonly store: IEventStore) {}
async recordProductionCritical(eventSourceId: string): Promise<void> { await this.store.eventLog.append( eventSourceId, new TaggingDynamicTagsEventOccurred('production issue'), { tags: ['production', 'critical'] }); }
async recordDevelopmentTest(eventSourceId: string): Promise<void> { await this.store.eventLog.append( eventSourceId, new TaggingDynamicTagsEventOccurred('test run'), { tags: ['development', 'testing'] }); }
async recordBatchMigration(eventSourceId: string): Promise<void> { await this.store.eventLog.append( eventSourceId, new TaggingDynamicTagsEventOccurred('batch migration'), { tags: ['migration', 'batch-process'] }); }}Using Tags
Section titled “Using Tags”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.