npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@monixlite/grammy-scenes

v1.4.0

Published

Scene middleware for grammY with step-based navigation

Downloads

664

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-scenes

Core 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 message and callbacks
  • 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.reply for 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:cancel is 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 null if 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.middleware
  • scenes.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