Event
A past-tense fact. Something the system observed and committed.
Events are immutable. They flow into actors (state transitions), into projections (read models), and into reactions (policies). They are the spine of an Nwire app.
Shape
import { defineEvent } from "@nwire/messages";
import { z } from "zod";
export const AnswerWasSubmitted = defineEvent({
name: "submissions.answer-was-submitted",
description: "Avi just tapped Submit.",
visibility: "public", // 'public' (default) | 'internal'
outcome: "milestone", // 'success' | 'failure' | 'warning' | 'milestone'
businessWeight: 10, // for dashboard weighting
audience: ["product", "ops"], // who cares — Studio filter
schema: z.object({
submissionId: z.string(),
studentId: z.string(),
exerciseId: z.string(),
submittedAt: z.string().datetime(),
}),
});
// In a handler — directly call the defined event as a factory:
return AnswerWasSubmitted({ submissionId, studentId, exerciseId, submittedAt: now });Naming — <Subject>Was<VerbPast>
Events are past-tense facts. The exported identifier should make that impossible to misread:
✅ AnswerWasSubmitted OrderWasPlaced EmailWasVerified
❌ SubmitAnswer PlaceOrder VerifyEmail (imperative — read as commands)
❌ AnswerSubmittedEvent OrderPlacedEvent (the noise suffix tells you nothing)Was reads aloud as a fact in any sentence: "AnswerWasSubmitted — when this happens, …". The language fights you if you accidentally write present tense, which is the point. See the conservation-of-meaning principle.
Visibility
'public'(default) — the event is part of the module's public surface; other modules MAY listen viawhen(SomeEvent, ...). Published on the bus in split topologies.'internal'— module-internal; cross-module listeners are rejected atcreateApptime. Use for implementation-detail events.
Outcome
Lets Studio aggregate success/failure rates without parsing event names:
'success'(default) — positive thing happened'failure'— domain-meaningful failure (not a system error)'milestone'— significant progress'warning'— anomaly worth surfacing
Flow
Handler ─returns event─▶ Runtime publish()
│
├─▶ Actor states[].on → state transition
├─▶ Projection on → read-model fold
├─▶ Reaction when() → policy fires
└─▶ Bus (if visibility: public)
│
└─▶ Telemetry: event.publishedIdempotency
Events DON'T have built-in idempotency. The actor's state machine handles "have I already seen this?" — typically by checking the actor's current state. For inbound events from external systems use defineInbox to dedup by message id.
See also
- defineEvent — full API
- Actor — state machines that fold events
- Projection — read views built from events
- Reaction — policies triggered by events