| 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.
- Trigger commands for further writes — inject
ICommandPipeline and execute a command. Never use IEventLog directly.
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 |