Skip to content

@puregram/flow

four primitives for "the bot needs to wait for something" use cases:

  • waitFor(kind, opts) — pause your handler until a matching update arrives. timeout-aware, filterable, cancellable
  • prompt(text, opts) — send a question and wait for the reply in one call
  • collectMediaGroup(opts) — gather every message in an album before continuing
  • persistent flows — same prompt/waitFor shape but the conversation state survives bot restarts, backed by any KVStorage

@puregram/flow supersedes @puregram/prompt from v2 and replaces the old mergeMediaEvents option

install

sh
yarn add @puregram/flow
sh
npm i -S @puregram/flow
sh
pnpm add @puregram/flow

then extend your Telegram instance once at startup:

ts
import { Telegram } from 'puregram'
import { flow } from '@puregram/flow'

const tg = Telegram.fromToken(process.env.TOKEN!)
  .extend(flow())

flow() accepts an options object — see options below

quick start

ts
import { Telegram } from 'puregram'
import { flow } from '@puregram/flow'

const tg = Telegram.fromToken(process.env.TOKEN!)
  .extend(flow())

tg.command('echo', async (message) => {
  await message.send('send me anything')

  const reply = await message.flow.waitFor('message', {
    timeout: 30_000,
    nullOnTimeout: true
  })

  if (reply === null) {
    return message.send('timed out')
  }

  return message.send(`you said: ${reply.text}`)
})

await tg.startPolling()

two surfaces — update.flow and tg.flow

the plugin exposes every primitive in two places:

surfacehow to accesswhen to use
update.flowmessage.flow, callbackQuery.flow, etc.inside handlers — chat and sender are auto-derived from the incoming update
tg.flowtelegram.flowoutside handlers — cron jobs, webhook endpoints, or when you want to override the auto-derived scope

update.flow is the form you want almost always. it auto-scopes waitFor and prompt to the same chat and the same sender so you never have to thread chatId / userId through the call:

ts
// inside a handler
await message.flow.prompt("what's your name?")   // auto-scoped to this chat + this sender

// outside a handler (or explicit override)
await tg.flow.prompt(chatId, "what's your name?", { from: userId })

update.flow is attached to every update kind that carries a chat id in its payload: message, edited_message, channel_post, edited_channel_post, business_message, edited_business_message, callback_query, chat_member, my_chat_member, chat_join_request, and all message-derived service events (new_chat_members, pinned_message, boost_added, and many more)

quick summary

every section below shows update.flow first, then the tg.flow equivalent. for handler code, always reach for update.flow

options

pass options to the flow() factory call:

ts
tg.extend(flow({
  mediaGroupWindow: 1_500,           // ms; default 1000
  storage: new RedisStorage(...),    // required for persistent flows
  defaultTtl: 24 * 60 * 60 * 1000   // ms; default: no expiry
}))
optiontypedefaultdescription
mediaGroupWindownumber (ms)1000sliding-window timeout used by collectMediaGroup
storageKVStorage<PersistedFlow>nonerequired for any persistent waitFor/prompt call. omit for ephemeral-only use
defaultTtlnumber (ms)noneTTL applied to persistent records when the call site doesn't pass ttl. absent and no per-call ttl means no expiry

see also