Skip to main content

The 3 types

before — before processing

Forgetting next() blocks the eventA before hook that returns without calling next() stops the chain and the event, exactly like ctx.abort(): the message is not routed (a 'send' is skipped, a provider event is not emitted). A hook that only observes or rewrites must always end with await next(). To block on purpose (rate limiting, filters) either return without next() or call ctx.abort() (see abort() below) — abort() reads more explicitly.On 'message', next() runs the remaining before hooks; routing (the command, conversation or handler) starts after every before hook has returned. To act after the handler, use an after hook.Call next() once. A hook that calls it again keeps the first dispatch (the rest of the chain runs once; every later next() returns the same promise) and the misuse is reported on the 'error' event.

after — after processing

onError — when something fails

There’s also a generic bot.hook(event, phase, handler, priority?) if you need to register a phase dynamically instead of calling before/after/ onError directly. A handler error is always emitted on bot.on('error', ({ error, ctx }) => ...), whether or not an onError('message') hook is registered.

Use cases

Logging

Chat ids are phone numbers or emails and bodies are private messages: keep them out of your logs, or hash them the way the bot does with logIds.

Rate limiting

Filter groups

For group messages the built-in groupMode option is usually simpler: it filters before the hooks even run.

Block users

For a real block list prefer the built-in bot.blacklist / bot.whitelist: they drop the message before it is even persisted and also cover button taps and calls. See Handoff & access lists.

Transform or block outgoing messages

'send' fires on every outbound send, whether it comes from ctx.reply/ctx.photo/… or from bot.send. The payload is { chatId, content, options } — content is the transport-neutral OutboundContent ({ kind: 'text', text }, { kind: 'image', media, caption }, …); mutate it to transform a message, or abort to skip the send entirely:
An aborted send resolves { skipped: true } instead of rejecting (a broadcast counts it as skipped).

Edits and streamed output

Some payloads carry a marker so a hook can tell them apart from a plain send: A hook can rewrite payload.content or abort() any of them. An aborted edit resolves skipped and sends no new message in its place. A degraded edit (the provider cannot edit) goes out as a normal send, without edit.

abort() — stop the pipeline

abort() stops the remaining before hooks and the event itself. Returning without next() has the same effect; abort() also works from code that has no next at hand (a helper that receives only ctx).

Priority

Typed contexts

The context a hook receives is typed by the event name (HookContextMap): 'message' gives a MessageContext, 'send' a SendHookContext ({ chatId, content, options?, stream?, edit? }), 'call' a CallEvent, 'reaction' an InboundReaction, and so on; before hooks also get abort(). No annotation is needed:
A custom event (one a plugin runs its own hooks for) is unknown unless you annotate the handler, pass the type argument, or augment HookContextMap (declaration merging):
HookHandler<TCtx = unknown> and HookErrorHandler<TCtx = unknown> name a handler’s type; a handler typed for one event (HookHandler<MessageContext>) cannot be registered on another ('send').

Available events

Provider-level connection events (qr, ready, connection, …) are plain bot.on(...) listeners, not hook phases — see Core concepts. The same goes for 'conversation:started' / 'conversation:ended' (see Operations).

Next steps