Skip to main content
Besides text, regex and predicate triggers, @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 with flow(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, a before hook that aborts or the groupMode filter do not spend the welcome.
  • Precedence: WELCOME runs before every other matcher, text triggers included. If the first message is “menu”, WELCOME handles it; call ctx.pass() so bot.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 (MemoryStore with file, SQLite, Redis, Postgres, MySQL or Mongo). Keys are hashes (welcome:<sha256>), with no ids or phone numbers in clear text. A custom IStore without 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 WELCOME handler, 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 === '' and ctx.call (the CallEvent: id, from, status, plus optional chatId, isGroup, isVideo and offline). ctx.reply() answers the caller. react/quote/markRead/delete make 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.sender is the caller, the conversation is per participant, and scope and groupMode apply.
  • It runs after the before/after('call') hooks and the 'call' event. A before('call', (ev) => ev.abort()) suppresses it (and the after('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, onMessage or the 'message' event, and are not stored as messages. They are ignored while the bot is stopped.
  • A CALL trigger that starts a conversation never replaces one in progress: if the caller already has an active conversation or a dormant checkpoint, the next CALL handler runs instead. Plain bot.when(CALL, fn) handlers still run mid-flow.
  • bot.onType('call', handler) is the same as bot.when(CALL, handler).
  • Testing: provider.call({ id: 'c1', from: 'user-1', status: 'offer' }) on MockProvider (@botmesh/core/testing).
Flows built with flow(WELCOME) or a type trigger get an anonymous id, so .persistent() needs an explicit .id('...').

Next steps