Skip to content

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

ts
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 via when(SomeEvent, ...). Published on the bus in split topologies.
  • 'internal' — module-internal; cross-module listeners are rejected at createApp time. 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.published

Idempotency

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

MIT licensed.