Hooks are lightweight event subscribers in BlueLibs Runner. They listen for events and react when those events are emitted. Unlike tasks, hooks are optimized for side effects and don’t return meaningful values.Think of hooks as “listeners” or “handlers” — when an event happens, hooks spring into action.
Hooks solve the problem of reacting to events without coupling the emitter to the listener. They let you:
Send notifications when users register
Track analytics when orders complete
Update caches when data changes
Trigger workflows when critical events occur
Hooks vs Tasks: Tasks are for business logic that returns a result. Hooks are for side effects that react to events. Use tasks when you need a return value, hooks when you’re responding to something that already happened.
A hook can stop propagation to prevent downstream hooks from running:
const validateUserHook = r .hook("app.hooks.validateUser") .on(userRegistered) .order(1) .run(async (event) => { if (!isValidEmail(event.data.email)) { event.stopPropagation(); // Stop other hooks throw new Error("Invalid email address"); } }) .build();const sendWelcomeEmail = r .hook("app.hooks.sendWelcomeEmail") .on(userRegistered) .order(10) .run(async (event, { mailer }) => { // This won't run if validation fails await mailer.send(event.data.email, "Welcome!", "Thanks for joining!"); }) .build();
When to stop propagation: Use it for validation hooks or circuit breakers. Once you call stopPropagation(), no hooks with higher order numbers will run.
By default, hooks run sequentially. Enable parallel execution on the event:
const userRegistered = r .event("app.events.userRegistered") .parallel(true) // Enable parallel execution .build();const sendWelcomeEmail = r .hook("app.hooks.sendWelcomeEmail") .on(userRegistered) .order(10) .run(async (event, { mailer }) => { await mailer.send(event.data.email, "Welcome!", "..."); }) .build();const trackAnalytics = r .hook("app.hooks.trackAnalytics") .on(userRegistered) .order(10) // Same order as sendWelcomeEmail .run(async (event, { analytics }) => { await analytics.track("user.registered", event.data); }) .build();
With .parallel(true):
Hooks with the same order run concurrently
Different order groups run sequentially
If one hook fails, all hooks in the batch complete, then an AggregateError is thrown
When to use parallel execution: Use it for independent side effects like notifications and analytics. Avoid it for operations that depend on each other or share mutable state.
Here’s a complete example showing multiple hooks reacting to a single event:
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(), name: z.string(), }) ) .build();
src/notifications/hooks/welcome-email.hook.ts
import { r } from "@bluelibs/runner";import { userRegistered } from "../../users/events";import { mailer } from "../resources/mailer.resource";export const sendWelcomeEmail = r .hook("app.hooks.sendWelcomeEmail") .meta({ title: "Send Welcome Email", description: "Sends a welcome email to newly registered users", }) .on(userRegistered) .order(10) .dependencies({ mailer }) .run(async (event, { mailer }) => { await mailer.send({ to: event.data.email, subject: "Welcome to Our Platform!", body: `Hi ${event.data.name}, thanks for joining us!`, }); }) .build();
src/analytics/hooks/track-registration.hook.ts
import { r } from "@bluelibs/runner";import { userRegistered } from "../../users/events";import { analytics } from "../resources/analytics.resource";export const trackRegistration = r .hook("app.hooks.trackRegistration") .meta({ title: "Track User Registration", description: "Sends registration event to analytics platform", }) .on(userRegistered) .order(20) .dependencies({ analytics }) .run(async (event, { analytics }) => { await analytics.track("user.registered", { userId: event.data.userId, timestamp: new Date(), }); }) .build();
src/slack/hooks/notify-team.hook.ts
import { r } from "@bluelibs/runner";import { userRegistered } from "../../users/events";import { slack } from "../resources/slack.resource";export const notifyTeam = r .hook("app.hooks.notifyTeam") .meta({ title: "Notify Team on Slack", description: "Posts a Slack message when a new user registers", }) .on(userRegistered) .order(30) .dependencies({ slack }) .run(async (event, { slack }) => { await slack.post({ channel: "#new-users", text: `New user registered: ${event.data.name} (${event.data.email})`, }); }) .build();
All three hooks run independently. If the Slack hook fails, the email and analytics hooks still run (assuming you use failureMode: "aggregate").