Skip to main content

Overview

The hook builder provides a fluent interface for defining event listeners. Hooks execute when their target event(s) are emitted, enabling reactive and decoupled architecture.
import { r, globals } from "@bluelibs/runner";

const sendWelcomeEmail = r.hook("app.hooks.sendWelcome")
  .on(userRegisteredEvent)
  .order(10)
  .dependencies(() => ({ emailService }))
  .run(async ({ payload, deps, logger }) => {
    await logger.info(`Sending welcome email to ${payload.email}`);
    await deps.emailService.send(payload.email, "Welcome!");
  })
  .build();

Methods

on()

Specifies which event(s) this hook listens to.
.on<TOn>(event: TOn)
event
IEvent | IEvent[] | '*'
required
The event(s) to listen to. Can be:
  • A single event definition
  • An array of event definitions
  • "*" to listen to all events
// Single event
.on(userRegisteredEvent)

// Multiple events
.on([orderCreated, orderUpdated, orderDeleted])

// All events (wildcard)
.on("*")
Returns: New builder with event target set (required before build())

run()

Defines the hook’s execution logic.
.run(
  fn: (context: HookContext<TPayload, TDeps>) => Promise<void>
)
fn
Function
required
The hook execution function. Receives a context object with:
  • payload - The event payload (typed based on the event)
  • event - The event definition that was emitted
  • deps - Resolved dependencies
  • runtime - Runtime instance
  • logger - Scoped logger
.run(async ({ payload, event, deps, runtime, logger }) => {
  await logger.info(`Handling ${event.id}`);
  await deps.notificationService.notify(payload);
  await runtime.runTask(updateMetricsTask, { event: event.id });
})
Returns: New builder with run function set (required before build())

order()

Sets the execution order for this hook.
.order(priority: number)
priority
number
default:"0"
Execution priority. Lower numbers run first. Hooks with the same order run concurrently if the event has parallel(true).
// Early hook (runs first)
.order(-10)

// Default priority
.order(0)

// Late hook (runs last)
.order(100)
Returns: New builder with order set

dependencies()

Defines dependencies to inject into the hook.
.dependencies<TDeps>(
  deps: TDeps | (() => TDeps),
  options?: { override?: boolean }
)
deps
DependencyMap | () => DependencyMap
required
Object mapping dependency keys to resources. Can be static or a function.
.dependencies(() => ({
  emailService: emailServiceResource,
  logger: globals.resources.logger,
  database: databaseResource,
}))
options.override
boolean
default:"false"
When true, replaces existing dependencies. When false (default), merges with existing.
Returns: New builder with updated dependencies type

tags()

Attaches tags for grouping and filtering.
.tags<TTags>(
  tags: TagType[],
  options?: { override?: boolean }
)
tags
TagType[]
required
Array of tag definitions.
.tags([criticalTag, asyncTag])
options.override
boolean
default:"false"
When true, replaces existing tags. When false (default), appends to existing.
Returns: New builder with tags attached

meta()

Attaches metadata for documentation and tooling.
.meta<TMeta>(metadata: ITaskMeta)
metadata
ITaskMeta
required
Metadata object with optional title and description.
.meta({
  title: "Send Welcome Email",
  description: "Sends a welcome email when user registers",
})
Returns: New builder with metadata attached

throws()

Documents which errors the hook may throw.
.throws(errorList: ThrowsList)
errorList
ThrowsList
required
Array of error classes. This is declarative only (for documentation).
.throws([EmailError, NetworkError])
Returns: New builder with error documentation

build()

Builds and returns the final hook definition.
.build(): IHook<TDeps, TOn, TMeta>
Returns: Immutable hook definition ready to register Throws: Error if on() or run() was not called

Type Signature

interface HookFluentBuilder<
  TDeps extends DependencyMapType = {},
  TOn extends ValidOnTarget | undefined = undefined,
  TMeta extends ITaskMeta = ITaskMeta
> {
  id: string;
  on<TNewOn extends ValidOnTarget>(on: TNewOn): HookFluentBuilder<TDeps, TNewOn, TMeta>;
  order(order: number): HookFluentBuilder<TDeps, TOn, TMeta>;
  dependencies<TNewDeps extends DependencyMapType>(
    deps: TNewDeps | (() => TNewDeps),
    options?: { override?: boolean }
  ): HookFluentBuilder<TDeps & TNewDeps, TOn, TMeta>;
  tags<TNewTags extends TagType[]>(
    t: TNewTags,
    options?: { override?: boolean }
  ): HookFluentBuilder<TDeps, TOn, TMeta>;
  meta<TNewMeta extends ITaskMeta>(m: TNewMeta): HookFluentBuilder<TDeps, TOn, TNewMeta>;
  throws(list: ThrowsList): HookFluentBuilder<TDeps, TOn, TMeta>;
  run(fn: IHookDefinition<TDeps, ResolvedOn<TOn>, TMeta>["run"]): HookFluentBuilder<TDeps, TOn, TMeta>;
  build(): IHook<TDeps, ResolvedOn<TOn>, TMeta>;
}

Examples

Basic Hook

const logEvent = r.hook("app.hooks.logEvent")
  .on(userRegisteredEvent)
  .run(async ({ payload, logger }) => {
    await logger.info(`User registered: ${payload.email}`);
  })
  .build();

Hook with Dependencies

const sendWelcomeEmail = r.hook("app.hooks.sendWelcome")
  .on(userRegisteredEvent)
  .dependencies(() => ({
    emailService: emailServiceResource,
    templateEngine: templateResource,
  }))
  .run(async ({ payload, deps, logger }) => {
    await logger.info(`Sending welcome email to ${payload.email}`);
    const content = await deps.templateEngine.render('welcome', payload);
    await deps.emailService.send(payload.email, content);
  })
  .build();

Hook with Order (Priority)

// High priority - runs first
const validateData = r.hook("app.hooks.validate")
  .on(dataReceivedEvent)
  .order(-100)
  .run(async ({ payload }) => {
    if (!isValid(payload)) throw new ValidationError();
  })
  .build();

// Normal priority
const processData = r.hook("app.hooks.process")
  .on(dataReceivedEvent)
  .order(0)
  .run(async ({ payload }) => {
    await processData(payload);
  })
  .build();

// Low priority - runs last
const logCompletion = r.hook("app.hooks.logComplete")
  .on(dataReceivedEvent)
  .order(100)
  .run(async ({ logger }) => {
    await logger.info('Data processing complete');
  })
  .build();

Hook Listening to Multiple Events

const trackUserActivity = r.hook("app.hooks.trackActivity")
  .on([userLoggedIn, userLoggedOut, userUpdatedProfile])
  .dependencies(() => ({ analytics: analyticsResource }))
  .run(async ({ payload, event, deps }) => {
    await deps.analytics.track(event.id, payload);
  })
  .build();

Wildcard Hook (All Events)

const debugLogger = r.hook("app.hooks.debugAll")
  .on("*")
  .run(async ({ event, payload, logger }) => {
    await logger.debug(`Event emitted: ${event.id}`, { payload });
  })
  .build();

Hook with Task Execution

const processOrderHook = r.hook("app.hooks.processOrder")
  .on(orderCreatedEvent)
  .dependencies(() => ({
    processOrderTask,
  }))
  .run(async ({ payload, runtime }) => {
    // Execute a task in response to event
    await runtime.runTask(processOrderTask, { orderId: payload.orderId });
  })
  .build();

Hook with Error Handling

const notifyAdmin = r.hook("app.hooks.notifyAdmin")
  .on(criticalErrorEvent)
  .dependencies(() => ({
    alertService: alertServiceResource,
  }))
  .throws([AlertError, NetworkError])
  .meta({
    title: "Notify Admin on Critical Error",
    description: "Sends alert to admin when critical errors occur",
  })
  .run(async ({ payload, deps, logger }) => {
    try {
      await deps.alertService.send(payload);
    } catch (error) {
      await logger.error('Failed to notify admin', { error });
      throw error; // Re-throw to mark hook as failed
    }
  })
  .build();

Conditional Hook Logic

const sendNotification = r.hook("app.hooks.notify")
  .on(orderStatusChanged)
  .dependencies(() => ({
    notificationService: notificationResource,
    userPreferences: preferencesResource,
  }))
  .run(async ({ payload, deps, logger }) => {
    const prefs = await deps.userPreferences.get(payload.userId);
    
    if (!prefs.emailNotifications) {
      await logger.debug('Skipping notification - user disabled emails');
      return;
    }
    
    await deps.notificationService.send(payload.userId, payload.status);
  })
  .build();

Hook Execution Model

Sequential Execution

Hooks execute in order (lowest to highest):
// Order: -10
const first = r.hook("first").on(event).order(-10).run(async () => {}).build();

// Order: 0 (default)
const second = r.hook("second").on(event).run(async () => {}).build();

// Order: 10
const third = r.hook("third").on(event).order(10).run(async () => {}).build();

// Execution: first → second → third

Parallel Execution

Hooks with same order run concurrently if event has parallel(true):
const event = r.event("myEvent").parallel(true).build();

// All have order 0 - run in parallel
const hook1 = r.hook("h1").on(event).run(async () => {}).build();
const hook2 = r.hook("h2").on(event).run(async () => {}).build();
const hook3 = r.hook("h3").on(event).run(async () => {}).build();

Error Handling

By default, hook errors are logged but don’t stop other hooks:
await runtime.emitEvent(myEvent, payload, {
  failureMode: 'continue', // Continue even if hooks fail (default)
  throwOnError: false, // Don't throw on hook failure (default)
});

// Or get detailed report:
const report = await runtime.emitEvent(myEvent, payload, { report: true });
if (report.failedListeners.length > 0) {
  console.error('Failed hooks:', report.failedListeners);
}

Build docs developers (and LLMs) love