@monixlite/grammy-scenes
v1.4.0
Published
Scene middleware for grammY with step-based navigation
Downloads
664
Maintainers
Readme
@monixlite/grammy-scenes
A lightweight finite state machine (FSM) engine for Telegram bots built on grammY. The library provides declarative, step-based scenes with deterministic transitions, session-backed state, and minimal runtime overhead.
The goal of the library is to stay simple and predictable: no hidden magic, no implicit state, and no tight coupling to bot logic. Everything is explicit and driven by return values.
Installation
npm install @monixlite/grammy-scenesCore Concepts
Scene
A scene is a logical flow (for example: auth, profile_edit, checkout).
Step
A step is a single state inside a scene. Steps are ordered in the same order they are declared.
FSM
Internally, the library builds a finite state machine:
- scene title → ordered step list
- step → action handlers
Basic Usage
const { Bot, session } = require("grammy");
const { Scenes } = require("@monixlite/grammy-scenes");
const bot = new Bot("..."); {
bot.use(session({
initial: () => ({
scene: null,
}),
}));
};
const scenes = new Scenes([
[
{
scene: {
title: "auth",
step: "phone",
},
action: {
enter: async (ctx) => {
await ctx.reply("• Enter your phone number:");
},
message: async (ctx) => {
ctx.session.phone = ctx.message.text.trim();
},
},
},
{
scene: {
title: "auth",
step: "confirm",
},
action: {
enter: async (ctx) => {
const { phone } = ctx.session;
await ctx.reply([
`• Your phone number: ${phone}`,
`• Is this correct?`,
].join("\n"), {
reply_markup: {
inline_keyboard: [
[
{
text: "• Yes",
callback_data: "confirm:yes",
},
{
text: "• No",
callback_data: "confirm:no",
},
],
],
},
});
},
callbacks: [
{
match: /^confirm:(yes|no)$/,
handler: async (ctx, match) => {
await ctx.answerCallbackQuery();
switch (match[1]) {
case "yes": {
const { phone } = ctx.session;
await ctx.reply(`• Thank you! Your phone number ${phone} has been saved.`);
return "stop";
};
default: {
return "^phone";
};
};
},
},
],
},
},
],
], {
["inline_keyboard:scene:cancel"]: {
enabled: true,
component: {
text: "• Cancel scene •",
callback_data: "scene:cancel",
},
},
});
bot.callbackQuery("scene:cancel", async (ctx) => {
ctx.session.scene = null;
await ctx.editMessageReplyMarkup(null);
await ctx.answerCallbackQuery();
await ctx.reply("• Scene cancelled");
});
bot.command("start", async (ctx) => {
return await scenes.enter(ctx, {
title: "auth",
step: "phone",
});
});
bot.use(scenes.middleware);
bot.start();Scene Definition
{
scene: {
title: string,
step: string,
},
action: {
enter?: (ctx) => Promise<void>,
update?: (ctx) => Promise<SceneResult | void>,
message?: (ctx) => Promise<SceneResult | void>,
callbacks?: Array<{
match: RegExp,
handler: (
ctx,
match: RegExpMatchArray,
) => Promise<SceneResult | void>,
}>,
},
}SceneResult determines the next transition.
Callbacks
enter(ctx)
Executed on step entry.
- Used for messages and keyboards
- Cancel button can be injected automatically
update(ctx)
Executed on any update while the step is active.
- Runs before
messageandcallbacks - Suitable for guards, timeouts, media
- Return value controls navigation
message(ctx)
Triggered on text messages during the step.
callbacks[]
CallbackQuery handlers matched by regexp. The handler return value follows standard transition rules.
Scene Control
scenes.enter(ctx, scene)
Forces entering a scene step.
Behavior:
- Writes to
ctx.session.scene - Resolves step
- Executes
enter - Temporarily patches
ctx.replyfor cancel button injection
Manual Termination
ctx.session.scene = null;Middleware
bot.use(session(...));
bot.use(scenes.middleware);The middleware:
- Resolves the active step
- Dispatches updates to callbacks
- Applies transitions
Must be registered after session middleware.
Step Navigation
Transitions are driven by callback return values.
Transitions
undefined,"next",">"→ next step"prev","<"→ previous step"stop","exit","!"→ terminate scene"^step"→ jump within current scene"^scene:step"→ jump to another scene{ step }→ absolute step{ scene: { title, step } }→ absolute scene
Termination
Scene termination sets:
ctx.session.scene = null;Options
Options are passed to the Scenes constructor and stored internally.
const scenes = new Scenes(scenes, options, DEV);action:enter:redefinition:reply
Controls whether ctx.reply is temporarily patched during enter.
When enabled, all ctx.reply calls inside enter:
- preserve existing inline keyboards
- automatically append the cancel button (if enabled)
{
["action:enter:redefinition:reply"]: {
enabled: true,
},
}Disable this option if you want full manual control over ctx.reply behavior inside enter.
inline_keyboard:scene:cancel
Automatically injects a cancel button into all enter replies.
{
["inline_keyboard:scene:cancel"]: {
enabled: true,
component: {
text: "Cancel",
callback_data: "scene:cancel",
},
},
}Notes:
- Only affects
enter(ctx)replies - Does not implement cancellation logic
- The handler for
scene:cancelis user-defined
Accessing Options at Runtime
Options passed to Scenes are stored internally and can be accessed via scenes.option(name).
This is useful when you need to reuse option configuration (for example, the cancel button component) outside of the scene lifecycle.
Example:
const cancel = scenes.option("inline_keyboard:scene:cancel");
if (cancel?.enabled) {
console.log(cancel.component);
// { text: 'Cancel', callback_data: 'scene:cancel' }
};Notes:
- Returns
nullif the option is not defined - The returned object is not cloned; treat it as read-only
API
new Scenes(scenes, options, DEV?)
Creates a scene manager backed by ScenesMiddleware.
Exports:
scenes.middlewarescenes.enter(ctx, scene)scenes.option(name)
DEV mode
When DEV is set to true, the middleware prints warnings to the console if:
- a transition points to a missing step
- there is no next / previous step in a scene
This mode is intended for development and debugging only.
License
MIT
