Skip to content

defineEvent

defineEvent({ ... }) declares a typed past-tense fact.

Lives in @nwire/messages (cross-package surface), not @nwire/forge.

Signature

ts
import { defineEvent } from "@nwire/messages"

function defineEvent<TSchema extends ZodTypeAny>(
  def: EventDefinition<TSchema>,
): EventDefinition<TSchema>

EventDefinition

ts
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

ValueEffect
"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

ValueStudio behaviorWhen to use
"success" (default)Green badge / count toward success rateNormal positive completion
"failure"Red badge / count toward failure rateDomain-meaningful failure (not a system error) — e.g., payment-declined
"milestone"Yellow / highlighted in big-picture viewsSignificant progress — lesson-completed, onboarding-finished
"warning"Amber / shows in attention feedAnomaly 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.

ts
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 tag

The 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

ts
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:

ts
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:

ts
// ✅ 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:

ts
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

MIT licensed.