ReadonlyloggerReadonlyreadyReadonlyblacklistSenders/chats whose inbound messages are dropped before anything else (not
persisted, no hooks, no routing, no 'message' event). Ids are matched exactly
against ctx.sender and ctx.from. See AccessList.
ReadonlywhitelistWhen enabled, only senders/chats on this list are processed (the blacklist still wins). Disabled by default. See Whitelist.
Adds the listener function to the end of the listeners array for the
event named eventName. No checks are made to see if the listener has
already been added. Multiple calls passing the same combination of eventName
and listener will result in the listener being added, and called, multiple
times.
server.on('connection', (stream) => {
console.log('someone connected!');
});
Returns a reference to the EventEmitter, so that calls can be chained.
By default, event listeners are invoked in the order they are added. The
emitter.prependListener() method can be used as an alternative to add the
event listener to the beginning of the listeners array.
import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a
The callback function
Adds a one-time listener function for the event named eventName. The
next time eventName is triggered, this listener is removed and then invoked.
server.once('connection', (stream) => {
console.log('Ah, we have our first user!');
});
Returns a reference to the EventEmitter, so that calls can be chained.
By default, event listeners are invoked in the order they are added. The
emitter.prependOnceListener() method can be used as an alternative to add the
event listener to the beginning of the listeners array.
import { EventEmitter } from 'node:events';
const myEE = new EventEmitter();
myEE.once('foo', () => console.log('a'));
myEE.prependOnceListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a
The callback function
Alias for emitter.removeListener().
Synchronously calls each of the listeners registered for the event named
eventName, in the order they were registered, passing the supplied arguments
to each.
Returns true if the event had listeners, false otherwise.
import { EventEmitter } from 'node:events';
const myEmitter = new EventEmitter();
// First listener
myEmitter.on('event', function firstListener() {
console.log('Helloooo! first listener');
});
// Second listener
myEmitter.on('event', function secondListener(arg1, arg2) {
console.log(`event with parameters ${arg1}, ${arg2} in second listener`);
});
// Third listener
myEmitter.on('event', function thirdListener(...args) {
const parameters = args.join(', ');
console.log(`event with parameters ${parameters} in third listener`);
});
console.log(myEmitter.listeners('event'));
myEmitter.emit('event', 1, 2, 3, 4, 5);
// Prints:
// [
// [Function: firstListener],
// [Function: secondListener],
// [Function: thirdListener]
// ]
// Helloooo! first listener
// event with parameters 1, 2 in second listener
// event with parameters 1, 2, 3, 4, 5 in third listener
Stop the bot. By default aborts conversations and tears down immediately.
Optionalopts: StopOptions
Options of Bot.stop.
Optionaldrain?: booleanWait for in-flight sends to complete before stopping (default false).
Optionaltimeout?: string | numberMax time to wait for the drain before forcing the stop: a duration ('5s') or ms. Default 10000.
Send normalized content to any chat (a plain string is sent as text, like
broadcast and sendTo). Runs the before('send')/after('send') hook
pipeline; a before('send') hook may mutate payload.content or abort() to skip.
Optionaloptions: SendOptionsDurable fire-and-forget send (needs BotConfig.durability): runs the before('send')
hooks, journals the send and returns its outbox key at once; the background sweeper
delivers it (retries, backoff, dead-letter queue). Enqueueing an idempotencyKey that
already exists is a no-op (status: 'exists') — use it for campaigns and scheduled
jobs that may run twice. A text over the channel's limit is split after the hooks
(BotConfig.splitLongText) into one record per part: key, key#2, ….
Optionaloptions: SendOptionsSend plain text to a chat. Same as send(chatId, text).
Use send: bot.send(chatId, text) (a string is sent as text).
Send content (a string is sent as text) to many chats: in batches of batchSize
(default 10) with concurrency parallel sends (default 1), pausing delayBetween
between batches. A RateLimitedError pauses the whole broadcast for its
retryAfterMs and retries that chat (up to retries times after the first send, default 3); other errors
are recorded and the broadcast goes on. Abort signal to stop early — never-attempted
chats come back in pending. bot.stop() cancels it the same way (with drain,
stop waits for its in-flight sends), and a broadcast started on a bot that is not
started (never started, or stopped) sends nothing. Duplicate ids are sent once. Each send takes the normal
outbound path (send hooks, ledger, the provider's rate limiter) but not the
dispatcher's per-chat queues, so inbound handling is never blocked; still, launch it
with void (not await) from inside a handler so that chat's queue is not held.
Optionalopts: BroadcastOptionsconst ac = new AbortController()
const res = await bot.broadcast(chatIds, '🎉 Nueva promo!', {
batchSize: 10, delayBetween: '2s', signal: ac.signal,
onProgress: (done, total) => bot.logger.info(`${done}/${total}`),
onFail: (chatId, error) => bot.logger.warn({ chatId, err: error }, 'broadcast failed'),
})
// → { total: 500, sent: 480, failed: 20, skipped: 0, errors: [{ chatId, error }], messages, cancelled: false, pending: [] }
Start a registered flow/conversation for a subject without waiting for them to
write — reminders from bot.schedule(), webhooks, admin panels. data seeds
c.state. The start goes through the same gates as a message (blacklist /
whitelist, human handoff) and the chat's FIFO queue; by default a subject that
already has a live or dormant conversation is left alone (ifActive: 'replace'
cancels it). Resolves once the conversation is entered — not when it ends
(listen to 'conversation:ended' for that). Rejects when name is not registered.
Safe to await from a handler or command of the same chat: the trigger then runs
inline in that message's queue slot instead of queuing behind it (which would
deadlock the chat). Awaiting a trigger for a DIFFERENT chat whose handlers in turn
await a trigger back into this one can still deadlock — launch those with void.
Before-message hooks, matchers and the 'message' event do not run, and nothing
is persisted for the synthetic inbound (ctx.raw is { synthetic: 'trigger', name }).
Snapshot of the runtime counters: conversations (started/completed/cancelled/
timedOut/retriesExceeded/errored, active, mean duration) overall and per flow,
messages in/out, inbound drops (duplicate/blocked/handoff), triggers, errors and
uptime. In-process only. Render it for Prometheus with toPrometheus().
Zero every metrics counter (uptime is kept).
Hand chatId over to a human agent. Until resume, the Dispatcher skips
every routing stage for that chat (commands, conversations, dormant resume,
matchers, catch-all) except BotConfig.handoff.allowCommands; inbound messages
are still persisted, hooks still run and the 'message' event still fires.
Active conversations in the chat are cancelled silently and its dormant
checkpoints dropped. Emits 'handoff' and persists the state on
StoredChat.handoff (survives restarts with a persistent store).
The state flips synchronously, so not awaiting is safe. Resolves true if the
chat was not already handed off; never rejects for store failures (they go to
the 'error' event). Throws TypeError synchronously on an empty chat id.
Optionalopts: HandoffOptionsGive a handed-off chat back to the bot: routing resumes immediately, 'resume'
fires and the persisted state is cleared. Resolves true if the chat was handed off.
Optionalopts: ResumeOptionsIs chatId currently handed off to a human?
Snapshot of the chat's handoff state (since, reason, meta), or undefined.
Snapshots of every chat currently handed off.
Send to a chat from outside the inbound pipeline — typically a human agent's
message during a handoff. Same path as send (before/after('send')
hooks, outbound ledger); it is the same call as send, named for handoff
code. Works whether or not the chat is handed off and never resumes the bot by itself.
Optionaloptions: SendOptionsUse send: bot.send(chatId, content, options) (the very same call).
Restore exportAccessLists output. Lists missing from snapshot are left untouched.
Route messages matching trigger (text, RegExp, function, type trigger such as
IMAGE, WELCOME, or an array) to a handler or a flow. Returns an unsubscriber.
Register an async conversation, entered when opts.trigger (default: name) matches.
ctx is the full MessageContext of the triggering message (ctx.tools, media
helpers, react…); c offers ask/waitText/waitMedia/choose, retry(),
idle()/idleClear(), end(farewell?) and goto(name).
Optionalopts: { trigger?: Trigger }Enter an inline ConversationHandler for the chat (or group participant) of ctx.
To start a REGISTERED conversation or flow, use trigger (the by-name form here is
deprecated).
Fire-and-forget by design: it returns immediately, so a handler can start a
conversation and return, releasing the chat's FIFO queue for the user's next
message (which the conversation then receives). Any conversation already active
on that key is replaced. Failures (e.g. an unknown name) are reported through
the 'error' event, never as an unhandled rejection.
The returned promise settles (it never rejects) once the conversation has ended
and released its key. Never await it inside a handler: that blocks the chat's
queue, so the user's answer could never reach the conversation.
Start a registered conversation with trigger:
bot.trigger(name, { chatId: ctx.from, userId: ctx.sender, isGroup: ctx.isGroup, ifActive: 'replace' })
(safe to await from a handler of the same chat).
Register flows (an array is accepted). Chainable; remove one later with unregisterFlow. Plugins go through plug.
Same as use(flow), typed for a single flow.
Use use (bot.use(flow)), or registerFlow to get the flow id
back and refuse a duplicate id.
Register a /name command (wins over an active conversation). Returns an unsubscriber.
Catch-all handler: receives the messages no command, conversation or matcher took.
Not to be confused with bot.on('message', …), the event that observes every
inbound message. Returns an unsubscriber.
Handle every message of one MessageType (same as when(IMAGE, handler) and the
other type triggers). Returns an unsubscriber.
Register a flow at runtime (same as use(flow), but refuses a duplicate id).
Returns the flow id. See replaceFlow to hot-swap an existing one.
Remove a flow or conversation by id: its triggers stop matching. Conversations already
running finish on the version they entered with. Returns false when unknown.
Hot-swap the flow registered under id (registers it when absent). The new flow keeps
id whatever its own id is; its trigger replaces the old one. Running conversations
keep the old version; persisted checkpoints resume on the new steps (by step index).
Whether a flow or conversation is registered under id.
Every live flow (kind: 'flow') and async conversation (kind: 'conversation').
Start flow id proactively in a chat, without an inbound message (the user's replies
then flow into it as usual). A thin wrapper over trigger with
ifActive: 'replace' — same access-list and handoff gates, per-chat FIFO slot,
language resolution, messaging seams and metrics — so it replaces a conversation
already running (or dormant) on the same key. Resolves false when the flow is
unknown, the subject is refused by the access lists (blacklist / whitelist) or the
chat is handed off; rejects when the bot is not started.
Use trigger: bot.trigger(id, { chatId, userId, isGroup, data, body, ifActive: 'replace' })
(it resolves a TriggerResult; result.started is this method's boolean).
Snapshots of every live conversation (key, state, waiting, queued).
Cancel the live conversation of target (a ConversationTarget, e.g. a ctx,
or a raw runtime key from listConversations). Returns false when none is live.
Re-arm the WELCOME trigger so the subject's next message is treated as a
first contact again (persisted in the store, so it survives restarts).
resetWelcome(dmChatId) — the user of that DM.resetWelcome(groupId, userId) — one participant of a group.resetWelcome(groupId) — every participant of that group.Called from a handler of the same chat it is ordered with that chat's
messages (the dispatcher's FIFO); called from elsewhere it races like any
last-writer-wins store write. Rejects if the store write fails (the
in-process state is re-armed regardless). slashCommandsPlugin calls it on /reset.
OptionaluserId: stringRead-only snapshot of the live conversation for a chat (and participant, in
groups): captured c.state, whether it waits for a reply. undefined when
none is active. Diagnostics only — e.g. botmesh dev's :state command.
True while the bot is still reacting to what it received: a provider event
is being stored or dispatched to its handlers (e.g. one awaiting an AI
call), or a conversation is running a step instead of waiting for the
user's reply. Work a handler starts without awaiting it is not tracked.
Diagnostics only — botmesh dev uses it to wait for slow replies when its
input is piped.
Register a before hook (higher priority runs first). Chainable. The context is typed
by the event (HookContextMap: MessageContext for 'message',
SendHookContext for 'send', …) plus abort(); a custom event's context is
unknown unless the handler annotates it (or TCtx is given).
Register an after hook (higher priority runs first). Chainable. Context typed like before.
Register an error-phase hook: (ctx, error) with the failing context. priority
orders it like before/after (default 0).
Optionalpriority: numberOptionalpriority: numberOptionalpriority: numberInstall a plugin (optionally with config). Unlike use, returns an
unsubscriber (not the bot: do not chain) that first calls the plugin's uninstall()
and then removes, newest first, what it registered through ctx.bot: commands,
matchers, conversations/flow triggers, hooks, nested plugins, runtime flows and
onDispose callbacks. Calling it again is a no-op. It does NOT undo event listeners
(on/once), provide()d tools, locale() messages, schedule/every/delay
jobs, onIdle handlers or a flow replaceFlow() overwrote: undo those in
ctx.onDispose().
install() should be synchronous. If it throws, whatever it registered so far is
rolled back and the error is rethrown; if it returns a promise that rejects, the
rejection goes to the 'error' event instead of crashing the process.
spec.requires lists plugin names that must already be plugged (a nested plugin may
require its parent); a missing one throws before install() runs. Flows registered
through ctx.bot.registerFlow() (or created by ctx.bot.replaceFlow()) are
unregistered on unplug.
One instance per plugin name: plugging a name that is already plugged throws (unplug
the first instance before plugging it again), unless every instance sets
spec.multiple: true.
Optionalconfig: Record<string, unknown>Register (deep-merge) the messages of locale code, or — with an options
object — configure i18n (defaultLocale, detect, providerLanguage,
userKey, locales). Either form turns on per-message language resolution:
ctx.user preference → provider-reported language → detect → default,
limited to the locales allow-list when one is set.
Keys nest (ctx.t('menu.title')), {param} placeholders are interpolated
and { one, other, zero? } leaves pick a plural form from params.count.
The built-in core messages live under core.* (es and en ship) and
can be overridden here. Throws TypeError on an invalid code or message tree.
bot.locale('es', { greeting: '¡Hola, {name}!', cart: { one: '{count} producto', other: '{count} productos' } })
bot.locale('en', { greeting: 'Hello, {name}!', cart: { one: '{count} item', other: '{count} items' } })
bot.locale({ defaultLocale: 'es' })
bot.when('hola', (ctx) => ctx.reply(ctx.t('greeting', { name: ctx.pushName ?? '' })))
bot.command('lang', async (ctx, [code]) => {
if (!code || !bot.locales().includes(code.toLowerCase())) return ctx.reply(bot.locales().join(', '))
await ctx.setLang(code)
})
Register (deep-merge) the messages of locale code, or — with an options
object — configure i18n (defaultLocale, detect, providerLanguage,
userKey, locales). Either form turns on per-message language resolution:
ctx.user preference → provider-reported language → detect → default,
limited to the locales allow-list when one is set.
Keys nest (ctx.t('menu.title')), {param} placeholders are interpolated
and { one, other, zero? } leaves pick a plural form from params.count.
The built-in core messages live under core.* (es and en ship) and
can be overridden here. Throws TypeError on an invalid code or message tree.
bot.locale('es', { greeting: '¡Hola, {name}!', cart: { one: '{count} producto', other: '{count} productos' } })
bot.locale('en', { greeting: 'Hello, {name}!', cart: { one: '{count} item', other: '{count} items' } })
bot.locale({ defaultLocale: 'es' })
bot.when('hola', (ctx) => ctx.reply(ctx.t('greeting', { name: ctx.pushName ?? '' })))
bot.command('lang', async (ctx, [code]) => {
if (!code || !bot.locales().includes(code.toLowerCase())) return ctx.reply(bot.locales().join(', '))
await ctx.setLang(code)
})
Translate outside a handler (scheduled jobs, broadcasts): lang defaults to the
default locale. Pair it with langOf to use a user's stored preference.
Optionalparams: TranslateParamsOptionallang: stringThe stored language preference of userId (ctx.setLang) when a registered locale
serves it, else the default locale — the same language ctx.lang would resolve from
the stored value (always the default while i18n is off). A store read failure
resolves the default (logged at debug level).
Registered locale codes, built-in es/en included.
Run fn on a cron expression (e.g. '0 9 * * *').
Durable cron task: its schedule is stored under opts.id (see BotConfig.scheduler),
survives restarts with the opts.missed policy, and each occurrence runs on one replica.
Cron task read in a time zone: bot.schedule('0 9 * * 1-5', { timeZone: 'America/Bogota' }, fn)
(see CronTaskOptions for the daylight-saving rules). Add an id to make it durable.
Run fn every interval (a duration such as '10m', or ms).
Durable interval task (see the durable schedule overload); slots do not drift.
Run fn once after duration (a duration such as '30s', or ms).
Durable one-shot task: its run time is fixed when opts.id is first stored, so a
restart does not push it back, provided the code registers the id again at boot (only
the schedule is stored, not the handler). See the durable schedule overload.
Delete the stored schedule of a durable task id (the bot's store namespace is
applied, as on registration): an orphan logged at start (stored, no longer registered
in code), a one-shot tombstone, or a registered task, which is cancelled as well.
Run handler once when a chat stays quiet for duration ('10m', '1h', '1d', ms…) after
its last processed inbound message. Every new inbound message from that chat reschedules it,
and re-arms it after it fired. ctx is that last message's context, so
ctx.reply('¿Sigues ahí?') answers in the right chat. Handler errors go to the bot
'error' event. Pending timers are cancelled by clearIdle, by the returned
unsubscribe and by stop(). The handler never runs for a chat in human handoff: a
'handoff' event carrying { chatId } cancels that chat's timers, and its messages do not
re-arm them until a 'resume' event for that chat (the bot's own isHandedOff() state is
honoured too, when present). duration must be positive and at most 2^31 − 1 ms (about
24.8 days, the longest a Node timer can wait); otherwise it throws RangeError.
Cancel pending onIdle timers for chatId (every chat when omitted) — e.g. when a
human agent takes the chat over. They re-arm on that chat's next processed inbound message.
OptionalchatId: stringPersistent profile of any subject — the same data a handler sees as
ctx.user when that subject writes (scheduled jobs, webhooks, admin code).
Requires a store implementing the user-data contract (every built-in one).
Physically remove expired ctx.user entries store-wide and resolve with
how many were removed (0 with native-TTL stores such as Redis). Writes
already trigger this at most once a minute; schedule it for idle bots:
bot.every('1h', () => bot.purgeExpiredUserData()).
Export normalized data associated with one subject in this bot namespace.
Delete normalized subject data in this bot namespace, including the handoff of chat
userId and the subject's live or dormant conversations.
Read-only snapshot of the registered flows, conversations and trigger routes,
plus the bits of config the linter needs. Input of describeFlows()/lintFlows().
Describe every registered flow/conversation as a Mermaid flowchart (default)
or, with { format: 'json' }, as a JSON graph. See describeFlows.
Describe every registered flow/conversation as a Mermaid flowchart (default)
or, with { format: 'json' }, as a JSON graph. See describeFlows.
Optionaloptions: DescribeOptions & { format?: "mermaid" }Describe every registered flow/conversation as a Mermaid flowchart (default)
or, with { format: 'json' }, as a JSON graph. See describeFlows.
Optionaloptions: DescribeOptionsStatically lint the registered flows: unreachable steps/flows, missing goto targets, empty/duplicate triggers, risky timeouts, ambiguous menus, … See lintFlows.
Optionaloptions: LintOptionsDurable message processing controls — the inbox/outbox journals, operator replay/retry
of dead-lettered records and counters. undefined unless BotConfig.durability is set.
The bot's tracing (BotConfig.telemetry): start your own spans under the bot's, or
check enabled. Plugins (@botmesh/ai) use it to add their spans to the same trace.
Without telemetry it is a disabled no-op (BotTracing.enabled false).
Resolves when the bot reaches the
'ready'connection state.