---
title: React to an event
editUrl: https://github.com/Cratis/Chronicle/edit/main/Documentation/scenarios/react-to-an-event.mdx
description: Run a side effect — send a notification, call an external system, append a follow-up event — when an event occurs.
---

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


**Goal:** when something happens — a book is returned, an order ships — you need to *do* something: notify a member, call an external API, or record a follow-up fact. That's a [reactor](/chronicle/reactors/).

## Write the reactor

A reactor is a class implementing the marker interface `IReactor`. You don't implement a method from it — you write a method whose **first parameter is the event type** you want to handle, and Chronicle routes matching events to it:

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

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

[EventType]
public record ScenariosReactBookReturned(string Isbn);

public interface IScenariosReactNotificationService
{
    Task NotifyNextInLine(EventSourceId bookId);
    Task NotifyNextInLine(EventSourceId bookId, string bookTitle);
}

public class ScenariosReactWaitlistNotifier(IScenariosReactNotificationService notifications) : IReactor
{
    public async Task BookReturned(ScenariosReactBookReturned @event, EventContext context)
    {
        // context.EventSourceId is the source the event happened to (the book)
        await notifications.NotifyNextInLine(context.EventSourceId);
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/basic-reactor.md)

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

```kotlin
import io.cratis.chronicle.events.EventContext
import io.cratis.chronicle.events.EventType
import io.cratis.chronicle.observation.Reactor

@EventType
data class ScenariosReactBookReturned(val isbn: String)

interface ScenariosReactNotificationService {
    fun notifyNextInLine(bookId: String)
    fun notifyNextInLine(bookId: String, bookTitle: String)
}

@Reactor
class ScenariosReactWaitlistNotifier(private val notifications: ScenariosReactNotificationService) {
    fun bookReturned(event: ScenariosReactBookReturned, context: EventContext) {
        // context.eventSourceId is the source the event happened to (the book)
        notifications.notifyNextInLine(context.eventSourceId)
    }
}
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/basic-reactor.md)

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

```java
import io.cratis.chronicle.events.EventContext;
import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.observation.Reactor;

@EventType
record ScenariosReactBookReturned(String isbn) {}

interface ScenariosReactNotificationService {
    void notifyNextInLine(String bookId);

    void notifyNextInLine(String bookId, String bookTitle);
}

@Reactor
class ScenariosReactWaitlistNotifier {
    private final ScenariosReactNotificationService notifications;

    ScenariosReactWaitlistNotifier(ScenariosReactNotificationService notifications) {
        this.notifications = notifications;
    }

    void bookReturned(ScenariosReactBookReturned event, EventContext context) {
        // context.getEventSourceId() is the source the event happened to (the book)
        notifications.notifyNextInLine(context.getEventSourceId());
    }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/scenarios/react-to-an-event/basic-reactor.md)

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

```elixir
defmodule MyApp.Events.ScenariosReactBookReturned do
  use Chronicle.Events.EventType, id: "scenarios-react-book-returned"

  defstruct [:isbn]
end

defmodule MyApp.ScenariosReactNotificationService do
  def notify_next_in_line(_book_id), do: :ok
  def notify_next_in_line(_book_id, _book_title), do: :ok
end

defmodule MyApp.Reactors.ScenariosReactWaitlistNotifier do
  use Chronicle.Reactors.Reactor

  alias MyApp.Events.ScenariosReactBookReturned
  alias MyApp.ScenariosReactNotificationService

  @handles ScenariosReactBookReturned

  @impl true
  def handle(%ScenariosReactBookReturned{}, context) do
    # context.event_source_id is the source the event happened to (the book)
    ScenariosReactNotificationService.notify_next_in_line(Map.get(context, :event_source_id))
    :ok
  end
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/basic-reactor.md)

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

```typescript
import { EventContext, eventType, onceOnly, reactor } from '@cratis/chronicle';
import { field } from '@cratis/fundamentals';

@eventType()
class ScenariosReactBookReturned {
    @field(String) readonly isbn: string;

    constructor(isbn: string) {
        this.isbn = isbn;
    }
}

interface ScenariosReactNotificationService {
    notifyNextInLine(bookId: string): Promise<void>;
    notifyNextInLine(bookId: string, bookTitle: string): Promise<void>;
}

@reactor()
class ScenariosReactWaitlistNotifier {
    constructor(private readonly notifications: ScenariosReactNotificationService) {}

    // Method name must be the exact camelCase of the event's class name -
    // Chronicle discovers handlers by name, not by parameter type.
    @onceOnly()
    async scenariosReactBookReturned(event: ScenariosReactBookReturned, context: EventContext): Promise<void> {
        // context.eventSourceId is the source the event happened to (the book)
        await this.notifications.notifyNextInLine(context.eventSourceId);
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/basic-reactor.md)

</TabItem>
</Tabs>

Chronicle discovers it by convention — no registration.

## Be safe to repeat

A reactor may run more than once for the same event (replay, recovery, redeploy). Make the side effect idempotent — record that it happened and skip if it already did. For a side effect that must never run again during a replay — a welcome email, a payment call — mark the handler (or the whole class) with `[OnceOnly]` and Chronicle excludes it from replays. See [OnceOnly](/chronicle/reactors/once-only/).

## Need state? Pick the right consistency

Reach for the event first — it carries the truth of what happened, and `context.EventSourceId` tells you what it happened *to*. But some reactions genuinely need more state. Say the notification should include the book's title — that lives in the read model, not the event.

:::tip
You *can* read state from a reactor — just mind the consistency level. A **materialized** read model (what the projection writes to the sink, and the default) is [eventually consistent](/chronicle/read-models/consistency/) and may not have caught up with the very event you're reacting to — whether you read it through `IReadModels.GetInstanceById` or take it as a handler parameter. Mark the read model `[Passive]` when the reaction needs it to include the event in your hand: Chronicle then computes it from the event log on every read.
:::

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

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

public record ScenariosReactBook(string Title);

public class ScenariosReactWaitlistNotifierWithTitle(IEventStore eventStore, IScenariosReactNotificationService notifications) : IReactor
{
    public async Task BookReturned(ScenariosReactBookReturned @event, EventContext context)
    {
        // Strongly consistent — rebuilt from the event log, includes this event
        var book = await eventStore.ReadModels.GetInstanceById<ScenariosReactBook>(context.EventSourceId);
        await notifications.NotifyNextInLine(context.EventSourceId, book.Title);
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/reading-state.md)

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

```kotlin
import io.cratis.chronicle.events.EventContext
import io.cratis.chronicle.observation.Reactor
import io.cratis.chronicle.readModels.ReadModel

@ReadModel
data class ScenariosReactBook(val title: String = "")

@Reactor
class ScenariosReactWaitlistNotifierWithTitle(private val notifications: ScenariosReactNotificationService) {
    // Looked up by the triggering event's event source id; a materialized read model can lag this event or be null.
    fun bookReturned(event: ScenariosReactBookReturned, context: EventContext, book: ScenariosReactBook?) {
        notifications.notifyNextInLine(context.eventSourceId, book?.title ?: "")
    }
}
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/reading-state.md)

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

```java
import io.cratis.chronicle.events.EventContext;
import io.cratis.chronicle.observation.Reactor;
import io.cratis.chronicle.readModels.ReadModel;

@ReadModel
record ScenariosReactBook(String title) {
    ScenariosReactBook() {
        this("");
    }
}

@Reactor
class ScenariosReactWaitlistNotifierWithTitle {
    private final ScenariosReactNotificationService notifications;

    ScenariosReactWaitlistNotifierWithTitle(ScenariosReactNotificationService notifications) {
        this.notifications = notifications;
    }

    // Looked up by the triggering event's event source id; a materialized read model can lag this event or be null.
    void bookReturned(ScenariosReactBookReturned event, EventContext context, ScenariosReactBook book) {
        notifications.notifyNextInLine(context.getEventSourceId(), book != null ? book.title() : "");
    }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/scenarios/react-to-an-event/reading-state.md)

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

```elixir
defmodule MyApp.ReadModels.ScenariosReactBook do
  use Chronicle.ReadModels.ReadModel

  defstruct title: ""
end

defmodule MyApp.Reactors.ScenariosReactWaitlistNotifierWithTitle do
  use Chronicle.Reactors.Reactor

  alias MyApp.Events.ScenariosReactBookReturned
  alias MyApp.ReadModels.ScenariosReactBook
  alias MyApp.ScenariosReactNotificationService

  @handles ScenariosReactBookReturned

  @impl true
  def handle(%ScenariosReactBookReturned{}, %{event_source_id: book_id}) do
    # Strongly consistent — rebuilt from the event log, includes this event
    {:ok, book} = Chronicle.read_model(ScenariosReactBook, book_id)
    ScenariosReactNotificationService.notify_next_in_line(book_id, book.title)
    :ok
  end
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/reading-state.md)

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

```typescript
import { EventContext, ReactorServices, readModel, reactor } from '@cratis/chronicle';

@readModel()
class ScenariosReactBook {
    title = '';
}

@reactor()
class ScenariosReactWaitlistNotifierWithTitle {
    // Constructor injection requires an artifactActivator in ChronicleOptions.
    constructor(private readonly notifications: ScenariosReactNotificationService) {}

    async scenariosReactBookReturned(event: ScenariosReactBookReturned, context: EventContext, services: ReactorServices): Promise<void> {
        // Eventually consistent: this read may not yet include the triggering event.
        const book = await services.readModels.findInstanceById(ScenariosReactBook, context.eventSourceId);
        if (book === null) throw new Error(`Book ${context.eventSourceId} is not available yet`);
        await this.notifications.notifyNextInLine(context.eventSourceId, book.title);
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/reading-state.md)

</TabItem>
</Tabs>

The full menu of read APIs — single, all, paged, observed — is in [Get read models](/chronicle/scenarios/real-time-query/).

## Appending an event

The common "translation" pattern — react to one slice's event by recording a new fact. The simplest way is to **return the event**: Chronicle appends it to the event log for you, against the triggering event's `EventSourceId`:

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

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

[EventType]
public record ScenariosReactBookReserved(string Isbn);

[EventType]
public record ScenariosReactStockDecreased(string Isbn, int Quantity);

public class ScenariosReactStockKeeping : IReactor
{
    // Without [OnceOnly], a replay of this reactor appends StockDecreased again.
    [OnceOnly]
    public ScenariosReactStockDecreased BookReserved(ScenariosReactBookReserved @event, EventContext context) =>
        new(@event.Isbn, 1);
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/return-event.md)

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

```kotlin
import io.cratis.chronicle.events.EventContext
import io.cratis.chronicle.events.EventType
import io.cratis.chronicle.observation.Reactor

@EventType
data class ScenariosReactBookReserved(val isbn: String)

@EventType
data class ScenariosReactStockDecreased(val isbn: String, val quantity: Int)

// Returning an event from a handler appends it as a side effect - against the event source that
// triggered the reactor - with no event log dependency in sight.
@Reactor
class ScenariosReactStockKeeping {
    fun bookReserved(event: ScenariosReactBookReserved, context: EventContext): ScenariosReactStockDecreased =
        ScenariosReactStockDecreased(event.isbn, 1)
}
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/return-event.md)

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

```java
import io.cratis.chronicle.events.EventContext;
import io.cratis.chronicle.events.EventType;
import io.cratis.chronicle.observation.Reactor;

@EventType
record ScenariosReactBookReserved(String isbn) {}

@EventType
record ScenariosReactStockDecreased(String isbn, int quantity) {}

// Returning an event from a handler appends it as a side effect - against the event source that
// triggered the reactor - with no event log dependency in sight.
@Reactor
class ScenariosReactStockKeeping {
    ScenariosReactStockDecreased bookReserved(ScenariosReactBookReserved event, EventContext context) {
        return new ScenariosReactStockDecreased(event.isbn(), 1);
    }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/scenarios/react-to-an-event/return-event.md)

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

```text
Elixir does not support this workflow yet.
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/return-event.md)

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

```typescript
import { EventContext, eventType, onceOnly, reactor } from '@cratis/chronicle';
import { field } from '@cratis/fundamentals';

@eventType()
class ScenariosReactBookReserved {
    @field(String) readonly isbn: string;

    constructor(isbn: string = '') {
        this.isbn = isbn;
    }
}

@eventType()
class ScenariosReactStockDecreased {
    @field(String) readonly isbn: string;
    @field(Number) readonly quantity: number;

    constructor(isbn: string = '', quantity: number = 0) {
        this.isbn = isbn;
        this.quantity = quantity;
    }
}

@reactor()
class ScenariosReactStockKeeping {
    // Without @onceOnly(), a replay of this reactor appends StockDecreased again.
    // Returning an event appends it for you, targeting the triggering event's own
    // event source id, stream, and subject - the simplest way to record a follow-up fact.
    @onceOnly()
    async scenariosReactBookReserved(event: ScenariosReactBookReserved, context: EventContext): Promise<ScenariosReactStockDecreased> {
        return new ScenariosReactStockDecreased(event.isbn, 1);
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/return-event.md)

</TabItem>
</Tabs>

`Task<StockDecreased>` and collections of events work too, and you can control the target event source — see [Returning side effects](/chronicle/reactors/side-effects/). Auto-appending a returned event is C#-only — every other client appends explicitly instead, shown next.

When you want to inspect the outcome yourself, append explicitly and handle the `AppendResult` — and **throw if it failed**, so the partition pauses instead of the fact being silently lost:

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

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

public class ScenariosReactStockCouldNotBeDecreased(string isbn) : Exception($"Stock could not be decreased for ISBN {isbn}");

public class ScenariosReactStockKeepingExplicit(IEventStore eventStore) : IReactor
{
    public async Task BookReserved(ScenariosReactBookReserved @event, EventContext context)
    {
        var result = await eventStore.EventLog.Append(
            context.EventSourceId, new ScenariosReactStockDecreased(@event.Isbn, 1));
        if (!result.IsSuccess)
        {
            throw new ScenariosReactStockCouldNotBeDecreased(@event.Isbn);
        }
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/explicit-append.md)

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

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/explicit-append.md)

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

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/scenarios/react-to-an-event/explicit-append.md)

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

```elixir
defmodule MyApp.Events.ScenariosReactBookReserved do
  use Chronicle.Events.EventType, id: "scenarios-react-book-reserved"

  defstruct [:isbn]
end

defmodule MyApp.Events.ScenariosReactStockDecreased do
  use Chronicle.Events.EventType, id: "scenarios-react-stock-decreased"

  defstruct [:isbn, :quantity]
end

defmodule MyApp.ScenariosReactStockCouldNotBeDecreased do
  defexception [:message]

  def exception(isbn) do
    %__MODULE__{message: "Stock could not be decreased for ISBN #{isbn}"}
  end
end

defmodule MyApp.Reactors.ScenariosReactStockKeepingExplicit do
  use Chronicle.Reactors.Reactor

  alias MyApp.Events.{ScenariosReactBookReserved, ScenariosReactStockDecreased}
  alias MyApp.ScenariosReactStockCouldNotBeDecreased

  @handles ScenariosReactBookReserved

  @impl true
  def handle(%ScenariosReactBookReserved{isbn: isbn}, %{event_source_id: event_source_id}) do
    case Chronicle.append(event_source_id, %ScenariosReactStockDecreased{isbn: isbn, quantity: 1}) do
      :ok -> :ok
      {:error, _reason} -> raise ScenariosReactStockCouldNotBeDecreased, isbn
    end
  end
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/explicit-append.md)

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

```typescript
import { EventContext, ReactorServices, onceOnly, reactor } from '@cratis/chronicle';

@reactor()
class ScenariosReactStockKeepingExplicit {
    @onceOnly()
    async scenariosReactBookReserved(event: ScenariosReactBookReserved, context: EventContext, services: ReactorServices): Promise<void> {
        const result = await services.eventStore.eventLog.append(
            context.eventSourceId, new ScenariosReactStockDecreased(event.isbn, 1));
        if (!result.isSuccess) {
            throw new Error(`Stock could not be decreased for ISBN ${event.isbn}`);
        }
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/scenarios/react-to-an-event/explicit-append.md)

</TabItem>
</Tabs>

## See also

- [Reactors](/chronicle/reactors/) — the full reactor model, filtering, and once-only handling.
- [Returning side effects](/chronicle/reactors/side-effects/) — every supported return shape and metadata control.
- [Projections, reducers, and reactors](/chronicle/concepts/observer-patterns/) — when a reactor is the right tool vs. building state.
