Publish a cross-module integration event via the Outbox and handle it idempotently in another module. Use when one module must react to something that happened in another. See .agents/rules/eventing.md.
61
72%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.agents/skills/add-integration-event/SKILL.mdCross-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.
Publishing needs no module registration — the outbox is framework-owned and the host wires it once. Inject IOutboxWriter (from FSH.Framework.Eventing.Abstractions) and add the event in the same unit of work:
public sealed class Do{Thing}CommandHandler({Source}DbContext db, IOutboxWriter 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.OutboxDrain.DrainAsync.IMultiTenantContextSetter (see WebhookFanoutHandler).Order in [assembly: FshModule]) — e.g. Notifications (750) before Chat (800).IIntegrationEvent, stable type nameIOutboxWriter.AddAsync (not the bus); no per-module eventing registration neededsealed : IIntegrationEventHandler<T>; AddIntegrationEventHandlers(assembly) registeredOrder lets the consumer load first3f2959e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.