defineEvent
defineEvent({ ... }) declares a typed past-tense fact.
Lives in @nwire/messages (cross-package surface), not @nwire/forge.
Signature
import { defineEvent } from "@nwire/messages"
function defineEvent<TSchema extends ZodTypeAny>(
def: EventDefinition<TSchema>,
): EventDefinition<TSchema>EventDefinition
interface EventDefinition<TSchema extends ZodTypeAny = ZodTypeAny> {
// Required
name: string // event id — unique across modules
schema: TSchema // zod object
// Optional contract
description?: string // surfaces in Studio + docs
visibility?: "public" | "internal" // default "public"
// Studio-aware metadata
outcome?: "success" | "failure" | "milestone" | "warning"
businessWeight?: number // 1=telemetry-chatter, 100=money-flow
audience?: readonly string[] // ['product', 'ops', 'finance']
}Visibility
| Value | Effect |
|---|---|
"public" (default) | Other modules MAY subscribe via defineWorkflow(...) and on(SomeEvent, …). Published to the bus when wire's publishToBus=true. |
"internal" | Module-internal. Cross-module subscribers are rejected at createApp. Never published to the bus. |
Default to public, narrow to internal
Events are part of your module's API. Make them public unless they're genuinely an implementation detail. Internal events are the right call for transient state-machine plumbing nobody outside the module should depend on.
Outcome — what Studio renders
| Value | Studio behavior | When to use |
|---|---|---|
"success" (default) | Green badge / count toward success rate | Normal positive completion |
"failure" | Red badge / count toward failure rate | Domain-meaningful failure (not a system error) — e.g., payment-declined |
"milestone" | Yellow / highlighted in big-picture views | Significant progress — lesson-completed, onboarding-finished |
"warning" | Amber / shows in attention feed | Anomaly worth surfacing — low-confidence-grade, unusual-spending-pattern |
defineEvent is the factory
defineEvent returns a callable that is the event definition. Call it to mint a validated EventMessage; read its meta fields (name, schema, outcome, …) directly.
export const AnswerWasSubmitted = defineEvent({
name: "submissions.answer-was-submitted",
schema: z.object({ studentId: z.string(), submittedAt: z.string().datetime() }),
})
// In a handler — call directly:
return AnswerWasSubmitted({ studentId: input.studentId, submittedAt: now() })
// → { eventName: "submissions.answer-was-submitted", payload: { ... } }
// Read meta fields:
AnswerWasSubmitted.name // "submissions.answer-was-submitted"
AnswerWasSubmitted.schema // the zod schema
AnswerWasSubmitted.outcome // optional outcome tagThe factory validates the payload against the zod schema. Wrong payload throws at call time, not at publish time — caught before any subscribers run. The legacy eventFactory(EventDef) form still works (it just rewraps), but you don't need it anymore.
EventPayload type helper
type EventPayload<E> = E extends EventDefinition<infer S> ? z.output<S> : never
const payload: EventPayload<typeof AnswerWasSubmitted>
// → { studentId: string; submittedAt: string }Useful when writing workflow subscriptions:
defineWorkflow("score-on-submission", ({ on, send }) => {
on(AnswerWasSubmitted, async (event) => {
// event is typed as EventPayload<typeof AnswerWasSubmitted>
await send(scoreSubmission, { studentId: event.studentId });
});
});Naming convention
Two parts:
1. The name field — <module>.<verb-past-tense>. Module prefix avoids collisions as modules grow: submissions.answer-was-submitted, orders.was-placed, users.email-was-verified.
2. The exported symbol — <Subject>Was<VerbPast>. Past-tense, PascalCase, the language fights you if you accidentally write present tense:
// ✅ Reads like English; conserves the workshop's vocabulary
export const AnswerWasSubmitted = defineEvent({
name: "submissions.answer-was-submitted",
schema: ...,
})
// ❌ Imperative-sounding — "is it a command? an event?"
export const SubmitAnswer = ...
export const AnswerSubmit = ...Why this matters: events are facts that already happened. The exported identifier should make that impossible to misread when a future reader sees AnswerWasSubmitted({ ... }) mid-handler. This is part of the conservation-of-meaning principle.
Multi-event handlers
A handler may return multiple events when one user action triggers two domain facts that aren't a single composite:
defineHandler(rejectArticle, async (input) => [
ArticleRejectedEvent({ articleId: input.id, reason: input.reason }),
AuthorPenalizedEvent({ authorId: input.authorId, points: -5 }),
])Both events flow through publish() in order. Use sparingly; usually the cleaner shape is one event per handler, with a reaction emitting the second.
See also
- Concepts → Event
- eventFactory
- defineWorkflow() — subscribe to events from anywhere
- defineProjection — fold events into read views