Shopping Flow
A flow with a numbered menu and branching using .choose().
The flow
import { Bot, flow } from '@botmesh/core'
import { BaileysProvider } from '@botmesh/baileys'
const bot = new Bot({
provider: new BaileysProvider({ session: 'demo-bot', qr: { terminal: true, small: true } }),
})
const catalog: Record<string, string> = {
electronics: '📱 *Electronics* — iPhone 15 ($999), Samsung S24 ($899), Xiaomi 14 ($599)',
clothing: '👕 *Clothing* — Shirt ($25), Jeans ($45), Jacket ($80)',
home: '🏠 *Home* — Lamp ($35), Rug ($60), Glass ($12)',
}
const buy = flow('buy')
.say('🛒 *What do you want to buy?*')
.choose('Write the option number', {
electronics: 'Electronics 📱',
clothing: 'Clothing 👕',
home: 'Home 🏠',
}, 'category')
.say((_ctx, c) => catalog[String(c.state.category)])
.say('✅ Thanks for your interest! Anything else?')
bot.use(buy)
await bot.start()How it works
- User sends "buy"
.say(...)sends the intro, then.choose()builds a numbered menu from theoptionskeys (1) Electronics 📱,2) Clothing 👕,3) Home 🏠) and waits for a valid number- The chosen KEY (
electronics,clothingorhome— not the number the user typed) is stored inc.state.category, since'category'is the 3rd argument to.choose() - The next
.say()readsc.state.categoryto print the matching catalog text - The flow closes with a thank-you message
Branch with then sub-steps
Instead of reading c.state afterwards, each option can run its own then step right when it's picked. Control returns to the parent flow once the sub-step finishes — there is no separate nested flow to build:
const buy = flow('buy')
.say('🛒 *What do you want to buy?*')
.choose('Write the option number', {
electronics: {
label: 'Electronics 📱',
then: (_ctx, c) => { c.state.summary = '📱 iPhone 15 — $999' },
},
clothing: {
label: 'Clothing 👕',
then: (_ctx, c) => { c.state.summary = '👕 Shirt — $25' },
},
home: {
label: 'Home 🏠',
then: (_ctx, c) => { c.state.summary = '🏠 Lamp — $35' },
},
})
.say((_ctx, c) => `${c.state.summary}\n\n✅ Thanks for your interest!`)
bot.use(buy)The chosen key is still available afterwards as c.state.choice (the default stateKey when you don't pass one) if you need it later in the chain.
Give the whole flow a timeout
.timeout() sets how long each wait in the flow (including the .choose() menu) waits for a reply. The message sent on timeout is NOT a .timeout() argument — configure it once on the bot:
const bot = new Bot({
provider: new BaileysProvider({ session: 'demo-bot' }),
conversation: { timeoutMessage: '⏰ Session expired due to inactivity.' },
})
const buy = flow('buy')
.timeout('2m')
.say('🛒 *What do you want to buy?*')
.choose('Write the option number', {
electronics: 'Electronics 📱',
clothing: 'Clothing 👕',
home: 'Home 🏠',
}, 'category')
.say((_ctx, c) => catalog[String(c.state.category)])
bot.use(buy)Back to the main menu
An option with goto leaves the current flow for another one; an option whose then is a whole flow runs it inline and comes back (like .use()):
const menu = flow('menu').choose('Main menu', { buy: 'Buy 🛒', help: 'Help' }, 'section')
const address = flow('address').ask('Street?', 'street').ask('City?', 'city')
const checkout = flow('checkout')
.choose('Confirm your order?', {
yes: { label: 'Yes ✅', then: address }, // runs the sub-flow, then continues below
back: { label: '⬅️ Main menu', goto: menu }, // leaves this flow
})
.say((_ctx, c) => `📦 Shipping to ${c.state.street}, ${c.state.city}`)
bot.use([menu, checkout]) // `address` runs inline, it needs no registrationbot.lint() reports a goto to a flow that is not registered (see Inspect & lint flows).
Next steps
- Support with timeout — timeout and fallback
- Flows — full DSL guide