Events are typed signals that tell your application “something happened.” They decouple parts of your app so they can communicate without tight coupling.Think of events as announcements: “User registered!” or “Order completed!” Other parts of your app can listen for these announcements and react accordingly, without the emitter knowing or caring who’s listening.
Define an event with a unique ID and a payload schema:
import { r } from "@bluelibs/runner";import { z } from "zod";const userRegistered = r .event("app.events.userRegistered") .payloadSchema( z.object({ userId: z.string(), email: z.string().email(), }) ) .build();
1
Create the event
Use r.event(id) with a unique namespaced ID like "app.events.userRegistered".
2
Add a payload schema (optional)
Call .payloadSchema(zodSchema) to validate the event payload at runtime.
3
Build it
Call .build() to finalize the event definition.
You can also use a type-only event without runtime validation:
If one hook fails, the entire batch completes, then an AggregateError is thrown
Use parallel execution for independent side effects (analytics, notifications) but keep it sequential for dependent operations (database writes, state updates).
Here’s a complete example showing events, hooks, and tasks working together:
src/users/events.ts
import { r } from "@bluelibs/runner";import { z } from "zod";export const userRegistered = r .event("app.events.userRegistered") .payloadSchema( z.object({ userId: z.string(), email: z.string().email(), }) ) .build();
src/users/tasks/register.task.ts
import { r } from "@bluelibs/runner";import { userRegistered } from "../events";export const registerUser = r .task("app.tasks.registerUser") .dependencies({ db, userRegistered }) .run(async (input, { db, userRegistered }) => { const user = await db.users.insert(input); await userRegistered({ userId: user.id, email: user.email }); return user; }) .build();
src/notifications/hooks/welcome-email.hook.ts
import { r } from "@bluelibs/runner";import { userRegistered } from "../../users/events";export const sendWelcomeEmail = r .hook("app.hooks.sendWelcomeEmail") .on(userRegistered) .dependencies({ mailer }) .run(async (event, { mailer }) => { await mailer.send({ to: event.data.email, subject: "Welcome!", body: "Thanks for joining our platform!", }); }) .build();
src/analytics/hooks/track-registration.hook.ts
import { r } from "@bluelibs/runner";import { userRegistered } from "../../users/events";export const trackRegistration = r .hook("app.hooks.trackRegistration") .on(userRegistered) .dependencies({ analytics }) .run(async (event, { analytics }) => { await analytics.track("user.registered", { userId: event.data.userId, }); }) .build();
Clean architecture: Notice how the registerUser task knows nothing about emails or analytics. Those concerns live in separate hooks, making the code easier to test and maintain.