| name | add-reactor |
| description | Use this skill when asked to add a Chronicle reactor (automation or translation) to a Cratis-based project. Reactors observe events and produce side effects. |
Add a Chronicle reactor that triggers automation or translation logic in response to events.
Always read .github/instructions/reactors.instructions.md first. It is the source of truth for reactor conventions, rules, and patterns.
Step 1 — Identify the event(s)
Determine which event type(s) the reactor needs to observe. These events must already exist or be created as part of the slice.
Step 2 — Create the reactor class
Place the reactor in the appropriate feature folder, co-located with the slice it belongs to.
namespace MyApp.Projects.Notifications;
public class ProjectRegisteredNotifier(INotificationService notifications) : IReactor
{
public async Task ProjectRegistered(Registration.ProjectRegistered @event, EventContext context) =>
await notifications.Notify($"Project '{@event.Name}' was registered.");
}
Step 3 — Follow the critical rules
IReactor is a marker interface — no methods to implement. Method dispatch is by first-parameter event type.
- Method name — can be anything descriptive. The name is for readability, not dispatch.
EventContext — optional second parameter. Omit if event metadata is not needed.
- Idempotent — reactors may be called more than once for the same event. Design accordingly.
- Use event data directly — never query the read model back inside the reactor.
- Return events instead of injecting IEventLog — to produce new events, return them directly as
Task<TEvent>, Task<ReactorSideEffect>, or Task<IEnumerable<ReactorSideEffect>>. For commands in other slices, inject ICommandPipeline. Avoid injecting IEventLog directly.
Step 3b — Return side-effect events (alternative to IEventLog)
Return events directly from a handler method instead of calling IEventLog.Append. This keeps reactors free of direct event-store dependencies.
public Task<StockDecreased> BookReserved(BookReserved @event, EventContext context) =>
Task.FromResult(new StockDecreased(@event.Isbn, 1));
public Task<IEnumerable<object>> BookReserved(BookReserved @event, EventContext context) =>
Task.FromResult<IEnumerable<object>>([new StockDecreased(@event.Isbn, 1), new StockLow(@event.Isbn)]);
public Task<ReactorSideEffect> BookReserved(BookReserved @event, EventContext context) =>
Task.FromResult(new ReactorSideEffect
{
Event = new StockDecreased(@event.Isbn, 1),
EventSourceId = @event.WarehouseId,
});
public Task<IEnumerable<ReactorSideEffect>> BookReserved(BookReserved @event, EventContext context) =>
Task.FromResult<IEnumerable<ReactorSideEffect>>(
[
ReactorSideEffect.For(new StockDecreased(@event.Isbn, 1)),
new ReactorSideEffect { Event = new StockLow(@event.Isbn), EventSourceId = @event.WarehouseId },
]);
Step 4 — Translation pattern (if applicable)
If the reactor adapts events from one slice by triggering commands in another, it is a Translation reactor:
public class StockKeeping(IStockKeeper stockKeeper, ICommandPipeline commandPipeline) : IReactor
{
public async Task BookReserved(BookReserved @event, EventContext context) =>
await commandPipeline.Execute(new DecreaseStock(@event.Isbn, await stockKeeper.GetStock(@event.Isbn)));
}
Step 5 — Filter by appended event metadata (optional)
If the reactor should only observe a subset of appended events, decorate it with filter attributes that match the metadata used when appending:
using Cratis.Chronicle;
using Cratis.Chronicle.Events;
[FilterEventsByTag("priority")]
[EventSourceType("order")]
[EventStreamType("fulfillment")]
public class PriorityFulfillmentReactor : IReactor
{
public Task OrderPlaced(OrderPlaced @event, EventContext context) => Task.CompletedTask;
}
These attributes correlate directly to the append call:
await eventLog.Append(
EventSourceId.New(),
new OrderPlaced(42m),
eventStreamType: "fulfillment",
eventSourceType: "order",
tags: ["priority"]);
[FilterEventsByTag] filters by event tags
[EventSourceType] filters by the appended event source type
[EventStreamType] filters by the appended event stream type
[Tag] and [Tags] still label the reactor itself; they do not filter events
For fuller examples, see Documentation/events/filtering/.
Step 6 — Validate
Run dotnet build. Fix all errors before completing.
When to use a Reactor vs a Projection
| Need | Use |
|---|
| Populate a queryable read model from events | Projection (see add-projection skill) |
| Trigger side effects, send notifications, call external APIs | Reactor |
| Adapt events across slices by triggering commands | Reactor (Translation pattern) |
| Both populate a read model AND trigger side effects | Both — one projection + one reactor |