prompt
prompt does three things atomically:
- sends
textto the chat - opens a
waitForscoped to that chat and the same sender - resolves with the matched reply (or
nullon timeout ifnullOnTimeout: true)
tg.command('signup', async (message) => {
const reply = await message.flow.prompt("what's your name?", {
timeout: 60_000,
nullOnTimeout: true
})
if (reply === null) {
return message.send('cancelled')
}
await message.send(`hi, ${reply.text}`)
})overriding the binding
by default update.flow.prompt pins to the same chat and the same sender. you can override either:
// "anyone in this chat can answer" — drop the sender pin
await message.flow.prompt('react below', { from: undefined })
// send the prompt to a different chat and wait for a reply there
await message.flow.prompt('reply over there', { chat: otherChatId })TIP
from: undefined set explicitly opts out of the sender pin. omitting from entirely keeps the default sender pin. the key has to be present in the options object to override
the lower-level form
tg.flow.prompt(chatId, text, options?) takes explicit chat and optional from:
await tg.flow.prompt(chatId, 'what is your name?', {
from: userId,
timeout: 60_000,
nullOnTimeout: true
})use this when you don't have an incoming update to bind to — webhook endpoints, scheduled outreach, or prompts triggered by an external event
options
prompt extends WaitForOptions with three extra knobs:
| field | type | default | description |
|---|---|---|---|
kind | keyof UpdateKindMap | 'message' | which update kind closes this prompt. set to 'callback_query' to wait for a button tap instead of a text reply |
from | number | sender of the source update | restrict to replies from a specific user id. pass undefined explicitly on update.flow.prompt to accept any user |
reply_markup | InlineKeyboardMarkup | ... | none | keyboard attached to the outgoing prompt message |
all WaitForOptions fields also apply: timeout, nullOnTimeout, consume, validate, transform, signal, and (on update.flow.prompt) match
chained multi-step prompts
each prompt call arms a fresh waitFor independently. answers from earlier prompts don't bleed into later ones:
tg.command('signup', async (message) => {
const name = await message.flow.prompt('name?', {
timeout: 60_000,
nullOnTimeout: true
})
if (name === null) {
return message.send('cancelled')
}
const age = await message.flow.prompt('age?', {
timeout: 60_000,
nullOnTimeout: true
})
if (age === null) {
return message.send('cancelled')
}
const email = await message.flow.prompt('your email?', {
timeout: 60_000,
nullOnTimeout: true
})
if (email === null) {
return message.send('cancelled')
}
await message.send(`registered: ${name.text}, ${age.text}, ${email.text}`)
})in-memory only
chained prompts like the above live entirely in memory. if the bot restarts between steps, the conversation is lost. use persistent flows for anything that must survive a restart
waiting for a button tap
set kind: 'callback_query' to close the prompt on a button press instead of a text message:
await message.flow.prompt('confirm?', {
kind: 'callback_query',
reply_markup: {
inline_keyboard: [[{ text: 'yes', callback_data: 'confirm:yes' }]]
},
timeout: 30_000,
nullOnTimeout: true
})errors
prompt delegates to waitFor internally, so it throws the same errors:
| error class | thrown when |
|---|---|
WaitForTimeout | timeout elapsed and nullOnTimeout is false |
WaitForCancelled | tg.flow.cancelAll() was called |
WaiterAbortedError | the signal option aborted |
see also
- waiters —
waitForand its full options surface - persistent flows — restart-safe
promptbacked by storage - @puregram/flow overview — install and quick start