NxCreateBlog
Feb 8, 2026·Guide·11 min read

A deep dive into Scenes

Multi-step conversation flows are the hardest part of bot development. This guide breaks down the scene primitives that hold up in practice.

Scenes are where many bots become fragile. The problem is usually not the concept itself — it is state management and unclear transitions. A scene that works perfectly in isolation starts misbehaving when users skip steps, go back unexpectedly, or send a command mid-flow.

This guide covers the patterns that hold up in production: how to keep state lean, how to make transitions explicit, how to handle recovery, and how to test scene flows before deploying.

What a scene actually is

A scene is a scoped context for handling a multi-step conversation flow. When a user enters a scene, subsequent messages from that user are routed to the scene handlers instead of the global bot handlers. The scene has its own state object that persists across steps.

const orderScene = new Scenes.WizardScene(
  'order_flow',
  // Step 1: ask for product
  async (ctx) => {
    await ctx.reply('Which product would you like to order?')
    return ctx.wizard.next()
  },
  // Step 2: ask for quantity
  async (ctx) => {
    ctx.wizard.state.product = ctx.message?.text
    await ctx.reply('How many units?')
    return ctx.wizard.next()
  },
  // Step 3: confirm and submit
  async (ctx) => {
    ctx.wizard.state.quantity = parseInt(ctx.message?.text ?? '0')
    await ctx.reply(`Order: ${ctx.wizard.state.quantity}x ${ctx.wizard.state.product}`)
    return ctx.scene.leave()
  }
)

Keep scene state small

Store only the minimum information needed to move the flow forward. Large state blobs make recovery harder and increase the chance of stale assumptions after edits. A common mistake is storing full database records in scene state when only an ID is needed.

Scene state is ephemeral by design. If the bot process restarts or the user's session expires, that state is gone. Anything important should be persisted to the database as soon as it is collected, with the scene state acting only as a working scratch pad.

  • Store IDs and primitive values, not full document records.
  • Persist collected data to MongoDB at each step, not just at the end.
  • Use a step field in state to know where the user is in the flow.
  • Clear state explicitly when leaving a scene to avoid leaks.

Prefer explicit transitions

A scene should make entry and exit conditions obvious. Avoid hidden branching that depends on unrelated handlers or implicit state mutations. If a step can route to two different next steps, that branching logic should live in the step handler itself.

// Explicit branching — easy to follow and test
async (ctx) => {
  const input = ctx.message?.text?.toLowerCase()
  if (input === 'cancel') {
    await ctx.reply('Order cancelled.')
    return ctx.scene.leave()
  }
  if (!['small', 'medium', 'large'].includes(input ?? '')) {
    await ctx.reply('Please reply with small, medium, or large.')
    return // stay on this step
  }
  ctx.wizard.state.size = input
  return ctx.wizard.next()
}

Handling commands mid-scene

Users will send /start or /cancel mid-flow. Without handling this, the command text is treated as regular input to the current step. The safest approach is a scene-level command listener that always exits cleanly.

orderScene.command('cancel', async (ctx) => {
  await ctx.reply('Cancelled. Use /start to begin again.')
  return ctx.scene.leave()
})

orderScene.command('start', async (ctx) => {
  await ctx.scene.leave()
  await ctx.reply('Restarting...')
  return ctx.scene.enter('order_flow')
})

Testing scene flows before shipping

The most reliable way to test scenes is to write a full interaction sequence and assert state at each step. NxCreator's hot-reload makes this fast: update the scene, trigger the entry command in your test bot, and walk through the flow. Keep a test Telegram account dedicated to this — do not use your own account or you will lose context constantly.

Test the happy path first, then test cancellation at each step, then test invalid input at each step. That three-pass approach covers the majority of production issues. The final thing to test is re-entry: what happens if a user enters the scene while already in it. Make sure the scene resets cleanly.

A scene that handles cancellation at every step and validates input at every step is production-ready. A scene that only handles the happy path isn't.
2026

Author

Areeb Khan

Keep readingView all