ObjectStackObjectStack

Events & Hooks

System-wide event bus for loose coupling between plugins

Events & Hooks

ObjectStack has two distinct hook systems. They look similar but use different APIs, payloads, ordering, and error semantics — pick the right one for the job:

  • Kernel lifecycle hooksctx.hook(name, handler) / ctx.trigger(name, ...args) on the PluginContext. Used for system bootstrap events and custom plugin-to-plugin events, matched by exact name.
  • Data lifecycle hooks — object-level hooks (beforeInsert, afterUpdate, …) registered via object Hook metadata or engine.registerHook(). They receive a single HookContext and run on record mutations.

Kernel Lifecycle Hooks

Lifecycle Events

Triggered by the Kernel during bootstrap and shutdown:

EventDescription
kernel:readyAll plugins have successfully started. System is live.
kernel:bootstrappedFired after every kernel:ready handler has settled, before kernel:listening. The "all bootstrap + seed data is ready" anchor — use it for reconcile/backfill work that consumes data a later-starting plugin produces during kernel:ready.
kernel:listeningFired after every kernel:ready and kernel:bootstrapped handler has completed (e.g. the HTTP server is accepting connections).
kernel:shutdownShutdown signal received. Plugins should clean up resources.

Listening to Kernel Events

ctx.hook('kernel:ready', async () => {
  ctx.logger.info('System is ready! Sending startup notification...');
});

ctx.hook(name, handler) takes exactly two arguments — there is no options/priority parameter. Kernel hooks run in registration order, and ctx.trigger() awaits each handler sequentially.

Emitting Custom Events

Plugins can trigger their own namespaced events for inter-plugin communication. Use ctx.trigger() (it is async and returns a Promise); handlers receive the positional arguments you pass to trigger():

// Trigger a custom business event (await it — trigger returns a Promise)
await ctx.trigger('order:shipped', { orderId: '123', carrier: 'fedex' });

// Another plugin listens — the handler receives the same positional args
ctx.hook('order:shipped', async ({ orderId, carrier }) => {
  await sendTrackingEmail(orderId, carrier);
});

Kernel hooks are matched by exact name — there is no wildcard or namespace-glob support. A handler registered under 'order:*' will never fire for 'order:shipped'.

Data Lifecycle Hooks

The Data Engine runs hooks around record reads and mutations. Write operations fire before* and after* events:

EventWhen
beforeInsert / afterInsertAround record creation.
beforeUpdate / afterUpdateAround record update.
beforeDelete / afterDeleteAround record deletion.

Read and bulk variants (beforeFind/afterFind, beforeCount, beforeAggregate, beforeUpdateMany, beforeDeleteMany, …) also exist.

The HookContext

Every data hook is a single-argument handler (ctx: HookContext) => void | Promise<void>. The context exposes:

FieldDescription
ctx.objectTarget object name (immutable).
ctx.eventCurrent lifecycle event, e.g. 'beforeInsert' (immutable).
ctx.inputMutable input. Shapes: insert { doc }, update { id, doc }, delete { id }. Modify this to change the operation.
ctx.resultOperation result, available in after* events (mutable).
ctx.previousRecord state before the operation (update/delete).
ctx.sessionAuth/tenancy info (userId, tenantId, roles, …).
ctx.apiScoped cross-object data access.

Registering Data Hooks

// Enrich a record before it is created
engine.registerHook('beforeInsert', async (ctx) => {
  if (ctx.object === 'order' && ctx.input.amount > 10000) {
    ctx.logger?.warn?.('Large order detected!');
    ctx.input.requires_approval = true;
  }
}, { object: 'order' });

// React after a record is created
engine.registerHook('afterInsert', async (ctx) => {
  await notifyWebhook(ctx.object, ctx.input.id);
}, { object: 'order' });

Error Handling

What happens when a data hook throws is governed by the hook's onError policy ('abort' | 'log', default 'abort'):

  • onError: 'abort' (default) — the error rolls back the transaction (when the hook is blocking), cancelling the operation.
  • onError: 'log' — the error is logged and execution continues; the operation is not cancelled.
engine.registerHook('beforeInsert', async (ctx) => {
  if (ctx.object === 'invoice' && !ctx.input.customer_id) {
    throw new Error('Invoice must have a customer'); // Aborts the insert (default onError)
  }
});

Ordering

Data hooks accept a priority option (default 100). They are sorted so that lower priority values run first:

engine.registerHook('beforeInsert', validate,  { priority: 50 });  // runs first
engine.registerHook('beforeInsert', enrich,    { priority: 100 }); // runs after

On this page