Appending with Tags
Chronicle lets you associate tags with appended events. Tags are stored as event metadata and can be used for categorization and filtering. There are two kinds: plain tags, which are simple strings, and named tags, which pair a name with a value.
The tags you add don’t take part in concurrency control. A concurrency scope narrows by the built-in metadata tags below and by event type.
Built-in metadata tags
Section titled “Built-in metadata tags”Chronicle maintains built-in metadata tags for event identity and stream routing:
EventSourceTypeEventSourceIdEventStreamTypeEventStreamId
You can add custom tags in addition to these built-in tags. For the full list and behavior, see Event Metadata Tags.
Custom tags
Section titled “Custom tags”Custom tags are simple strings that you provide when appending. They are merged with any static tags defined on the event type.
Append with tags
Section titled “Append with tags”using Cratis.Chronicle.Events;
[EventType]public record TaggedOrderPlaced(string CustomerId, decimal Total);
public class TaggedCheckoutService(IEventLog eventLog){ public Task<AppendResult> PlaceOrder(OrderId orderId, string customerId, decimal total) { return eventLog.Append( orderId, new TaggedOrderPlaced(customerId, total), tags: ["checkout", "priority"]); }}import io.cratis.chronicle.IEventStoreimport io.cratis.chronicle.eventSequences.AppendOptionsimport io.cratis.chronicle.eventSequences.AppendResultimport io.cratis.chronicle.events.EventType
@EventTypedata class TaggedOrderPlaced(val customerId: String, val total: Double)
class TaggedCheckoutService(private val eventStore: IEventStore) { suspend fun placeOrder(orderId: String, customerId: String, total: Double): AppendResult = eventStore.eventLog.append( orderId, TaggedOrderPlaced(customerId, total), AppendOptions(tags = listOf("checkout", "priority")) )}import io.cratis.chronicle.IEventStore;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.AppendOptionsBuilder;import io.cratis.chronicle.java.BlockingEventStore;
@EventTyperecord TaggedOrderPlaced(String customerId, double total) {}
class TaggedCheckoutService { private final BlockingEventStore eventStore;
TaggedCheckoutService(IEventStore eventStore) { this.eventStore = new BlockingEventStore(eventStore); }
void placeOrder(String orderId, String customerId, double total) { var options = new AppendOptionsBuilder().tag("checkout").tag("priority").build(); eventStore.getEventLog().append(orderId, new TaggedOrderPlaced(customerId, total), options); }}defmodule MyApp.Events.TaggedOrderPlaced do use Chronicle.Events.EventType, id: "tagged-order-placed"
defstruct [:customer_id, :total]end
defmodule MyApp.TaggedCheckoutService do alias MyApp.Events.TaggedOrderPlaced
def place_order(order_id, customer_id, total) do Chronicle.append( order_id, %TaggedOrderPlaced{customer_id: customer_id, total: total}, tags: ["checkout", "priority"] ) endendimport { eventType, IEventStore } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class TaggedOrderPlaced { @field(String) readonly customerId: string; @field(Number) readonly total: number;
constructor(customerId: string, total: number) { this.customerId = customerId; this.total = total; }}
class TaggedCheckoutService { constructor(private readonly store: IEventStore) {}
async placeOrder(orderId: string, customerId: string, total: number): Promise<void> { await this.store.eventLog.append( orderId, new TaggedOrderPlaced(customerId, total), { tags: ['checkout', 'priority'] }); }}AppendMany with tags
Section titled “AppendMany with tags”AppendMany applies the provided tags to each event in the batch.
using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.Events;
public readonly record struct TaggedAccountId(string Value){ public static implicit operator EventSourceId(TaggedAccountId id) => new(id.Value);}
[EventType]public record TaggedMoneyWithdrawn(decimal Amount);
[EventType]public record TaggedMoneyDeposited(decimal Amount);
public class TaggedTransferService(IEventLog eventLog){ public Task<AppendManyResult> Transfer(TaggedAccountId fromAccount, TaggedAccountId toAccount, decimal amount) { var events = new[] { new EventForEventSourceId(fromAccount, new TaggedMoneyWithdrawn(amount)), new EventForEventSourceId(toAccount, new TaggedMoneyDeposited(amount)) };
return eventLog.AppendMany(events, tags: ["transfer", "audit"]); }}import io.cratis.chronicle.IEventStoreimport io.cratis.chronicle.eventSequences.AppendResultimport io.cratis.chronicle.eventSequences.EventForEventSourceIdimport io.cratis.chronicle.events.EventType
@EventTypedata class TaggedMoneyWithdrawn(val amount: Double)
@EventTypedata class TaggedMoneyDeposited(val amount: Double)
class TaggedTransferService(private val eventStore: IEventStore) { suspend fun transfer(fromAccount: String, toAccount: String, amount: Double): List<AppendResult> { val events = listOf( EventForEventSourceId(fromAccount, TaggedMoneyWithdrawn(amount), tags = listOf("transfer", "audit")), EventForEventSourceId(toAccount, TaggedMoneyDeposited(amount), tags = listOf("transfer", "audit")) )
return eventStore.eventLog.appendMany(events) }}import io.cratis.chronicle.IEventStore;import io.cratis.chronicle.eventSequences.AppendResult;import io.cratis.chronicle.eventSequences.EventForEventSourceId;import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.java.BlockingEventStore;
import java.util.List;
@EventTyperecord TaggedMoneyWithdrawn(double amount) {}
@EventTyperecord TaggedMoneyDeposited(double amount) {}
class TaggedTransferService { private final BlockingEventStore eventStore;
TaggedTransferService(IEventStore eventStore) { this.eventStore = new BlockingEventStore(eventStore); }
List<AppendResult> transfer(String fromAccount, String toAccount, double amount) { var events = List.of( new EventForEventSourceId(fromAccount, new TaggedMoneyWithdrawn(amount), null, null, null, List.of("transfer", "audit")), new EventForEventSourceId(toAccount, new TaggedMoneyDeposited(amount), null, null, null, List.of("transfer", "audit")));
return eventStore.getEventLog().appendMany(events); }}defmodule MyApp.Events.TaggedMoneyWithdrawn do use Chronicle.Events.EventType, id: "tagged-money-withdrawn"
defstruct [:amount]end
defmodule MyApp.Events.TaggedWithdrawalFeeCharged do use Chronicle.Events.EventType, id: "tagged-withdrawal-fee-charged"
defstruct [:amount]end
defmodule MyApp.TaggedWithdrawalService do alias MyApp.Events.TaggedMoneyWithdrawn alias MyApp.Events.TaggedWithdrawalFeeCharged
def withdraw(account_id, amount, fee) do Chronicle.append_many( account_id, [ %TaggedMoneyWithdrawn{amount: amount}, %TaggedWithdrawalFeeCharged{amount: fee} ], tags: ["withdrawal", "audit"] ) endendimport { eventType, EventForEventSourceId, IEventStore } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class TaggedMoneyWithdrawn { @field(Number) readonly amount: number;
constructor(amount: number) { this.amount = amount; }}
@eventType()class TaggedMoneyDeposited { @field(Number) readonly amount: number;
constructor(amount: number) { this.amount = amount; }}
class TaggedTransferService { constructor(private readonly store: IEventStore) {}
async transfer(fromAccountId: string, toAccountId: string, amount: number): Promise<void> { const events: EventForEventSourceId[] = [ { eventSourceId: fromAccountId, event: new TaggedMoneyWithdrawn(amount) }, { eventSourceId: toAccountId, event: new TaggedMoneyDeposited(amount) } ];
await this.store.eventLog.appendMany(events, { tags: ['transfer', 'audit'] }); }}Named tags
Section titled “Named tags”A named tag pairs a name with an exact value, for example checkout-session = c-81f2. Use one when you want to find events by a runtime identifier later. Tagging explains the matching rules and how to query by named tag.
Append with named tags
Section titled “Append with named tags”Use AppendWithNamedTags for a tagged append; the existing Append call remains unchanged.
using Cratis.Chronicle;using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;
[EventType]public record NamedTaggedOrderPlaced(decimal Total);
public class NamedTaggedCheckoutService(IEventLog eventLog){ public Task<AppendResult> PlaceOrder(EventSourceId orderId, string checkoutSessionId, decimal total) => eventLog.AppendWithNamedTags( orderId, new NamedTaggedOrderPlaced(total), namedTags: [ new NamedTag("checkout-session", checkoutSessionId), new NamedTag("sales-channel", "web") ], tags: ["checkout"]);}import { eventType, IEventStore, NamedTag } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class NamedTaggedOrderPlaced { @field(Number) readonly total: number;
constructor(total: number) { this.total = total; }}
class NamedTaggedCheckoutService { constructor(private readonly store: IEventStore) {}
async placeOrder(orderId: string, checkoutSessionId: string, total: number): Promise<void> { await this.store.eventLog.append( orderId, new NamedTaggedOrderPlaced(total), { namedTags: [ new NamedTag('checkout-session', checkoutSessionId), new NamedTag('sales-channel', 'web') ], tags: ['checkout'] }); }}Named tags are stored separately from plain tags. You can pass both on the same append, and neither affects the other.
AppendMany with named tags
Section titled “AppendMany with named tags”Use AppendManyWithNamedTags when supplying call-level named tags.
Each event in a batch can carry its own named tags, and the append call can add named tags that apply to every event. Chronicle merges the two for each event:
- An exact duplicate of a name and value pair is kept once.
- The same name with a different value stays as a separate tag.
using Cratis.Chronicle;using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;
[EventType]public record NamedTaggedMoneyWithdrawn(decimal Amount);
[EventType]public record NamedTaggedMoneyDeposited(decimal Amount);
public class NamedTaggedTransferService(IEventLog eventLog){ public Task<AppendManyResult> Transfer(EventSourceId fromAccount, EventSourceId toAccount, decimal amount, string transferId) { EventForEventSourceId[] events = [ new(fromAccount, new NamedTaggedMoneyWithdrawn(amount)) { NamedTags = [new NamedTag("ledger-side", "debit")] }, new(toAccount, new NamedTaggedMoneyDeposited(amount)) { NamedTags = [new NamedTag("ledger-side", "credit")] } ];
// The withdrawal carries ledger-side = debit and transfer = <transferId>. // The deposit carries ledger-side = credit and transfer = <transferId>. return eventLog.AppendManyWithNamedTags( events, namedTags: [new NamedTag("transfer", transferId)], tags: ["transfer"]); }}import { eventType, EventForEventSourceId, IEventStore, NamedTag } from '@cratis/chronicle';import { field } from '@cratis/fundamentals';
@eventType()class NamedTaggedMoneyWithdrawn { @field(Number) readonly amount: number;
constructor(amount: number) { this.amount = amount; }}
@eventType()class NamedTaggedMoneyDeposited { @field(Number) readonly amount: number;
constructor(amount: number) { this.amount = amount; }}
class NamedTaggedTransferService { constructor(private readonly store: IEventStore) {}
async transfer(fromAccountId: string, toAccountId: string, amount: number, transferId: string): Promise<void> { const events: EventForEventSourceId[] = [ { eventSourceId: fromAccountId, event: new NamedTaggedMoneyWithdrawn(amount), namedTags: [new NamedTag('ledger-side', 'debit')] }, { eventSourceId: toAccountId, event: new NamedTaggedMoneyDeposited(amount), namedTags: [new NamedTag('ledger-side', 'credit')] } ];
// The withdrawal carries ledger-side = debit and transfer = <transferId>. // The deposit carries ledger-side = credit and transfer = <transferId>. await this.store.eventLog.appendMany(events, { namedTags: [new NamedTag('transfer', transferId)], tags: ['transfer'] }); }}Transactional appends through a unit of work accept named tags the same way.
Named tags are plaintext and are not encrypted by the compliance features. Don’t put secrets or personal data in them.