.agents/skills/add-integration-event/SKILL.md
Cross-module communication goes through integration events + the Outbox (transactional, crash-safe) —
never a direct in-process call into another module's runtime, and never IEventBus.PublishAsync from a
handler. Full model: .agents/rules/eventing.md.
Modules.{Source}.Contracts/Events/{Event}IntegrationEvent.cs — implement IIntegrationEvent:
public sealed record {Event}IntegrationEvent(
Guid Id,
DateTime OccurredOnUtc,
string? TenantId,
string CorrelationId,
string Source,
Guid {Entity}Id,
string SomePayload) : IIntegrationEvent;
⚠️ Don't rename/move this type later — the outbox stores its assembly-qualified name; a rename makes
Type.GetType() return null and the message dead-letters. Keep the type name + namespace stable.
The source module must have eventing wired (add-module Step 1): AddEventingCore + AddEventingForDbContext<{Source}DbContext>. Inject IOutboxStore and add the event in the same unit of work:
public sealed class Do{Thing}CommandHandler({Source}DbContext db, IOutboxStore outbox)
: ICommandHandler<Do{Thing}Command, Unit>
{
public async ValueTask<Unit> Handle(Do{Thing}Command command, CancellationToken cancellationToken)
{
// … mutate entities, db.SaveChangesAsync …
var evt = new {Event}IntegrationEvent(
Id: Guid.CreateVersion7(),
OccurredOnUtc: DateTime.UtcNow,
TenantId: /* current tenant */,
CorrelationId: Guid.NewGuid().ToString(),
Source: "{Source}",
{Entity}Id: entity.Id,
SomePayload: "…");
await outbox.AddAsync(evt, cancellationToken).ConfigureAwait(false);
return Unit.Value;
}
}
The OutboxDispatcherHostedService later publishes it via IEventBus.
Modules.{Consumer}/IntegrationEventHandlers/{Event}IntegrationEventHandler.cs — sealed, implement IIntegrationEventHandler<T>:
public sealed class {Event}IntegrationEventHandler({Consumer}DbContext db /*, IHubContext<AppHub> hub */)
: IIntegrationEventHandler<{Event}IntegrationEvent>
{
public async Task HandleAsync({Event}IntegrationEvent @event, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(@event);
// … write to the consumer's tables / push a notification …
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
}
Register the consumer's handlers in its ConfigureServices:
builder.Services.AddIntegrationEventHandlers(typeof({Consumer}Module).Assembly);
{eventId, handlerName}) — don't hand-roll it.IMultiTenantContextSetter (see WebhookFanoutHandler).Order in [assembly: FshModule]) — e.g. Notifications (750) before Chat (800).IIntegrationEvent, stable type nameAddEventingCore + AddEventingForDbContext<T>; published via IOutboxStore.AddAsync (not the bus)sealed : IIntegrationEventHandler<T>; AddIntegrationEventHandlers(assembly) registeredOrder lets the consumer load first