@botmesh/core exports trigger
constants for a subject’s first contact, for message types (images,
voice notes, locations…) and for incoming calls. They work everywhere
a trigger is accepted: bot.when(), flow() (including .scope()) and
bot.conversation(name, fn, { trigger }).
WELCOME
WELCOME fires on a subject’s first interaction with the bot:
- Scope: per chat in a DM and per participant in a group (each member
gets their own welcome), regardless of
conversationScope. A DM and a group count as separate first contacts. Limit it to DMs withflow(WELCOME).scope('dm')or{ ...WELCOME, scope: 'dm' }. - What counts as first contact: the first message that reaches the
matcher stage. A command (
/help), an active conversation, a dormant checkpoint resume, abeforehook that aborts or thegroupModefilter do not spend the welcome. - Precedence:
WELCOMEruns before every other matcher, text triggers included. If the first message is “menu”,WELCOMEhandles it; callctx.pass()sobot.when('menu')also gets it. - Persistence: stored through the store’s cursor key/value (every
bundled store supports it), so it survives restarts with a persistent
store (
MemoryStorewithfile, SQLite, Redis, Postgres, MySQL or Mongo). Keys are hashes (welcome:<sha256>), with no ids or phone numbers in clear text. A customIStorewithout cursors falls back to an in-process cache that does not survive restarts. - Concurrency: decided inside the per-chat FIFO queue, so two simultaneous first messages never get two welcomes. Across processes sharing one store it is check-then-set: route each chat to one process, as conversations already require.
- Cost: with no
WELCOMEhandler, zero extra store reads. With one, at most one read (two in groups) plus one write per user and process; after that an in-memory cache answers. - If the store read fails, the bot does not greet (fails closed), the error
goes to
bot.on('error')and the message continues normally.
Re-arming the welcome
slashCommandsPlugin calls resetWelcome on /reset (for the DM, or for
the participant who sent it). /cancel only cancels the conversation; the
welcome stays spent. bot.deleteSubject(userId) also re-arms the deleted
user’s welcome. exportSubject does not include this flag.
Message type triggers
Precedence
Matchers run by specificity (highest first; ties go to the one registered first):WELCOME (100) > equals / plain string (40) > startsWith (30) >
endsWith (25) > RegExp (20) > contains (10) > predicate function (5) >
single type (IMAGE…, bot.onType) (2) > multi-type
(MEDIA, typeTrigger(a, b)) (1) > catch-all (bot.onMessage).
A text trigger is never shadowed by a type trigger: an image with the
caption “hola” goes to bot.when('hola'). bot.onType('image') and
bot.when(IMAGE) share the same tier.
A trigger that is undefined or null (an unset environment variable, a
missing import), alone, inside an array or as { pattern }, throws a
TypeError when it is registered. So does an empty or blank text pattern
('', ' ', ['hola', ''], { pattern: '' }): it would match every
message without text (a photo without a caption), or every message with
contains/startsWith/endsWith. Use IMAGE/MEDIA for media and
bot.onMessage() for a catch-all. [] is still accepted: it never matches and
is the usual trigger of a flow entered only by goto(). bot.lint() reports
it (as info for a goto-only flow), and the ESLint rule reports every empty
trigger before the code runs.
CALL
CALL reacts to an incoming voice or video call:
- It fires once per call id (WhatsApp emits offer, then ringing,
reject, timeout or terminate). To see every update, listen to
bot.on('call', (ev) => ...). - The context has
type === 'call',body === ''andctx.call(theCallEvent:id,from,status, plus optionalchatId,isGroup,isVideoandoffline).ctx.reply()answers the caller.react/quote/markRead/deletemake no sense here. - A 1:1 call is handled as the caller’s DM. A group call is projected
into the group: replies go to the group,
ctx.senderis the caller, the conversation is per participant, andscopeandgroupModeapply. - It runs after the
before/after('call')hooks and the'call'event. Abefore('call', (ev) => ev.abort())suppresses it (and theafter('call')hooks and the'call'event). The blacklist, whitelist and handoff gates run before the hooks: a blocked call runs no'call'hooks and emits no'call'event either. - Calls never reach commands, text or function triggers,
onMessageor the'message'event, and are not stored as messages. They are ignored while the bot is stopped. - A
CALLtrigger that starts a conversation never replaces one in progress: if the caller already has an active conversation or a dormant checkpoint, the nextCALLhandler runs instead. Plainbot.when(CALL, fn)handlers still run mid-flow. bot.onType('call', handler)is the same asbot.when(CALL, handler).- Testing:
provider.call({ id: 'c1', from: 'user-1', status: 'offer' })onMockProvider(@botmesh/core/testing).
flow(WELCOME) or a type trigger get an anonymous id, so
.persistent() needs an explicit .id('...').
Next steps
- Core concepts: where each trigger runs
- Handoff & access lists
- Persistent user data
- Starting a flow programmatically:
bot.trigger()without an inbound message