---
title: Appended event metadata and projections
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

Projections build read models by mapping events to fields. They observe all events of the types declared in their definition — they do not use `[FilterEventsByTag]`, `[EventSourceType]`, or `[EventStreamType]` to filter incoming events.

## How projections select their input

A projection declares the event types it observes through its event mappings:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;

[EventType]
public record FilteringOrderPlaced(string CustomerId, decimal TotalAmount);

[EventType]
public record FilteringOrderShipped(DateTimeOffset ShippedAt);

[FromEvent<FilteringOrderPlaced>]
[FromEvent<FilteringOrderShipped>]
public record FilteringOrderSummary(
    [Key] string CustomerId,
    decimal TotalAmount,
    DateTimeOffset? ShippedAt);
```

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.events.EventType
import io.cratis.chronicle.projections.FromEvent
import io.cratis.chronicle.readModels.ReadModel
import java.math.BigDecimal
import java.time.OffsetDateTime

@EventType
data class FilteringOrderPlaced(val customerId: String, val totalAmount: BigDecimal)

@EventType
data class FilteringOrderShipped(val shippedAt: OffsetDateTime)

@ReadModel
@FromEvent(FilteringOrderPlaced::class)
@FromEvent(FilteringOrderShipped::class)
data class FilteringOrderSummary(
    val customerId: String = "",
    val totalAmount: BigDecimal = BigDecimal.ZERO,
    val shippedAt: OffsetDateTime? = null
)
```

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.projections.FromEvent;
import io.cratis.chronicle.readModels.ReadModel;
import java.math.BigDecimal;
import java.time.OffsetDateTime;

@EventType
record FilteringOrderPlaced(String customerId, BigDecimal totalAmount) {}

@EventType
record FilteringOrderShipped(OffsetDateTime shippedAt) {}

@ReadModel
@FromEvent(eventType = FilteringOrderPlaced.class)
@FromEvent(eventType = FilteringOrderShipped.class)
class FilteringOrderSummary {
    public String customerId = "";
    public BigDecimal totalAmount = BigDecimal.ZERO;
    public OffsetDateTime shippedAt = null;
}
```

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.Events.FilteringOrderPlaced do
  use Chronicle.Events.EventType, id: "filtering-order-placed"

  defstruct [:customer_id, :total_amount]
end

defmodule MyApp.Events.FilteringOrderShipped do
  use Chronicle.Events.EventType, id: "filtering-order-shipped"

  defstruct [:shipped_at]
end

defmodule MyApp.ReadModels.FilteringOrderSummary do
  use Chronicle.ReadModels.ReadModel

  defstruct customer_id: nil, total_amount: nil, shipped_at: nil

  from MyApp.Events.FilteringOrderPlaced,
    set: [customer_id: :customer_id, total_amount: :total_amount]

  from MyApp.Events.FilteringOrderShipped,
    set: [shipped_at: :shipped_at]
end
```

</TabItem>
<TabItem label="TypeScript">

```typescript
import { eventType, fromEvent, readModel } from '@cratis/chronicle';

@eventType()
class FilteringOrderPlaced {
    customerId = '';
    totalAmount = 0;
}

@eventType()
class FilteringOrderShipped {
    shippedAt: Date | null = null;
}

@readModel()
@fromEvent(FilteringOrderPlaced)
@fromEvent(FilteringOrderShipped)
class FilteringOrderSummary {
    customerId = '';
    totalAmount = 0;
    shippedAt: Date | null = null;
}
```

</TabItem>
</Tabs>

This projection receives every `OrderPlaced` and `OrderShipped` event regardless of any metadata attached during append. Metadata such as tags or stream type does not affect which events flow into a projection.

## Tagging projections

`[Tag]` and `[Tags]` on a projection label the projection definition for organizational purposes. They do not filter incoming events:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle;
using Cratis.Chronicle.Projections;

public record FilteringOrderReport(string CustomerId);

// Labels the projection for discoverability — does not affect which events are received
[Tag("reporting")]
public class FilteringOrderReportingProjection : IProjectionFor<FilteringOrderReport>
{
    public void Define(IProjectionBuilderFor<FilteringOrderReport> builder) =>
        builder.From<FilteringOrderPlaced>(b => b.UsingKey(e => e.CustomerId));
}
```

</TabItem>
</Tabs>

:::note[Client coverage]
Labeling a **projection** is currently C#-only. Kotlin and Java have the equivalents for a **reactor or reducer** — `@Tag`, `@FilterEventsByTag`, `@EventSourceType`, and `@EventStreamType` — but nothing on projection registration. Elixir and TypeScript have no tag or filter mechanism on any of the three.
:::

## Combining a projection with metadata-based filtering

When you need a side effect or secondary read model that reacts only to a subset of events based on appended metadata, pair the projection with a reactor or reducer that carries the appropriate filter attributes.

The following example shows an `OrderSummaryProjection` that builds the full read model for all orders, alongside a `PremiumOrderNotifier` reactor that fires only when an order is appended with the `premium` tag:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;
using Cratis.Chronicle.Reactors;

[EventType]
public record FilteringWithReactorOrderPlaced(string CustomerId, decimal TotalAmount);

// --- Append call ---
// Carries the "premium" tag for orders that qualify
// eventLog.Append(orderId, new FilteringWithReactorOrderPlaced(customerId, total), tags: ["premium"]);

// --- Projection: receives every OrderPlaced ---
[FromEvent<FilteringWithReactorOrderPlaced>]
public record FilteringWithReactorOrderSummary(
    [Key] string CustomerId,
    decimal TotalAmount);

// --- Reactor: receives only premium-tagged OrderPlaced ---
[FilterEventsByTag("premium")]
public class FilteringWithReactorPremiumOrderNotifier : IReactor
{
    public Task Placed(FilteringWithReactorOrderPlaced @event, EventContext context) =>
        Task.CompletedTask;
}
```

</TabItem>
</Tabs>

The same pattern works with a reducer instead of a reactor:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reducers;

public record FilteringPremiumOrderTotals(int Count, decimal Total);

[FilterEventsByTag("premium")]
public class FilteringPremiumOrderTotalsReducer : IReducerFor<FilteringPremiumOrderTotals>
{
    public FilteringPremiumOrderTotals Placed(FilteringWithReactorOrderPlaced @event, FilteringPremiumOrderTotals? current, EventContext context) =>
        new((current?.Count ?? 0) + 1, (current?.Total ?? 0m) + @event.TotalAmount);
}
```

</TabItem>
</Tabs>

Use this pattern whenever you need both a projection-based read model (covering all events) and a metadata-filtered view or side effect (covering a subset).

## Appending with metadata

The metadata you provide at append time drives the filters on any accompanying reducers or reactors:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;

public class FilteringAppendService(IEventLog eventLog)
{
    public async Task AppendOrders(string customerId)
    {
        // Appends to all observers — no extra metadata
        await eventLog.Append(EventSourceId.New(), new FilteringWithReactorOrderPlaced(customerId, 42m));

        // Appends to all observers; additionally dispatched to observers filtering on "premium"
        await eventLog.Append(EventSourceId.New(), new FilteringWithReactorOrderPlaced(customerId, 299m), tags: ["premium"]);

        // Appends with stream type; dispatched to observers filtering on "wholesale" stream type
        await eventLog.Append(EventSourceId.New(), new FilteringWithReactorOrderPlaced(customerId, 1500m), eventStreamType: "wholesale");
    }
}
```

</TabItem>
<TabItem label="Kotlin">

```text
Kotlin does not support this workflow yet.
`AppendOptions` only carries a `correlationId` — there is no way to attach tags
or a custom event stream type when appending from Kotlin. Track the client SDK
issue before relying on metadata-filtered observers from Kotlin.
```

</TabItem>
<TabItem label="Java">

```text
Java does not support this workflow yet.
`AppendOptions` only carries a `correlationId` — there is no way to attach tags
or a custom event stream type when appending from Java. Track the client SDK
issue before relying on metadata-filtered observers from Java.
```

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.Events.FilteringMetadataOrderPlaced do
  use Chronicle.Events.EventType, id: "filtering-metadata-order-placed"

  defstruct [:customer_id, :total_amount]
end

defmodule MyApp.FilteringAppendService do
  alias MyApp.Events.FilteringMetadataOrderPlaced

  def append_orders(order_id, customer_id) do
    # Appends to all observers — no extra metadata
    Chronicle.append(order_id, %FilteringMetadataOrderPlaced{customer_id: customer_id, total_amount: 42})

    # Appends to all observers; additionally dispatched to observers filtering on "premium"
    Chronicle.append(
      order_id,
      %FilteringMetadataOrderPlaced{customer_id: customer_id, total_amount: 299},
      tags: ["premium"]
    )

    # Appends with stream type; dispatched to observers filtering on "wholesale" stream type
    Chronicle.append(
      order_id,
      %FilteringMetadataOrderPlaced{customer_id: customer_id, total_amount: 1500},
      event_stream_type: "wholesale"
    )
  end
end
```

</TabItem>
<TabItem label="TypeScript">

```text
TypeScript does not support this workflow yet.
`AppendOptions` only carries `correlationId`, `eventSourceId`, and `concurrencyScope` —
there is no way to attach tags or a custom event stream type when appending from
TypeScript. Track the client SDK issue before relying on metadata-filtered
observers from TypeScript.
```

</TabItem>
</Tabs>

## See also

- [Filter reactors by appended event metadata](/chronicle/reactors/filtering/)
- [Filter reducers by appended event metadata](/chronicle/reducers/filtering/)
- [Tagging Projections](/chronicle/projections/tagging-projections/)
