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 listeners (reactions). 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.",
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(),
}),
});The definition is a callable factory — call it in a handler to mint a validated event message:
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.
Local by default; .public() to cross
An event is local by default — listeners, actors, and projections in its own app react to it, and it never leaves the boundary. To make an event part of the app's contract with other bounded contexts, mark it:
import { defineEvent } from "@nwire/messages";
import { z } from "zod";
export const OrderWasPlaced = defineEvent({
name: "orders.order-was-placed",
schema: z.object({ orderId: z.string() }),
}).public();.public() returns a marked clone (the original definition stays unmarked), and it gates exactly one thing: the boundary. An action's ctx.publish refuses an event without the marker, so an internal fact can never leak across it. See events and listeners for the emit / publish split and outbound sinks for how a published event leaves the process.
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 / ctx.publish─▶ shared publish path
│
├─▶ LocalDelivery chain:
│ idempotency → actors → projections → workflows
├─▶ `when` listeners (local fanout)
├─▶ outbound sink / bus — only if `.public()`
└─▶ telemetry: event.published
Handler ─ctx.emit─▶ same local fold + listeners, never the outbound leg
└─▶ telemetry: event.emittedIdempotency
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
- Events and listeners —
emit,publish, and listener files - Actor — state machines that fold events
- Projection — read views built from events