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

grammy-fsm

v0.2.0

Published

Finite State Machine (FSM) plugin for Grammy Telegram bot framework with type-safe state management

Readme

grammy-fsm

A full-featured Finite State Machine (FSM) implementation for the Grammy bot framework, inspired by Aiogram's FSM.

Features

  • Multiple Storage Backends: Memory (development) and Redis (production)
  • Type-Safe State Management: Define states using classes or constants
  • Flexible Data Storage: Store and retrieve user-specific data
  • Middleware Filters: Filter handlers based on current state
  • State Change Callbacks: React to state transitions
  • TTL Support: Automatic cleanup of expired states (Redis)
  • Fully Typed: Complete TypeScript support
  • Zero Dependencies: Only requires Grammy

Installation

npm install grammy-fsm
# or
yarn add grammy-fsm
# or
pnpm add grammy-fsm
# or
bun add grammy-fsm

Quick Start

import { Bot, Context } from "grammy";
import { createFSM, state, type FSMFlavor } from "grammy-fsm";

// 1. Extend context with FSM flavor
type MyContext = Context & FSMFlavor;

// 2. Define your states using enum
enum RegistrationStates {
  AwaitingName = "awaiting_name",
  AwaitingAge = "awaiting_age",
}

// 3. Create bot with extended context
const bot = new Bot<MyContext>("YOUR_BOT_TOKEN");

// 4. Initialize FSM plugin
bot.use(createFSM({ storage: "memory" }));

// 5. Start registration flow
bot.command("start", async (ctx) => {
  await ctx.reply("What's your name?");
  ctx.state = RegistrationStates.AwaitingName;
});

// 6. Handle name input
bot.filter(state(RegistrationStates.AwaitingName)).on("message:text", async (ctx) => {
  const name = ctx.message.text;

  ctx.data.name = name;
  await ctx.reply("How old are you?");
  ctx.state = RegistrationStates.AwaitingAge;
});

// 7. Handle age input
bot.filter(state(RegistrationStates.AwaitingAge)).on("message:text", async (ctx) => {
  const age = parseInt(ctx.message.text);

  ctx.data.age = age;

  const data = ctx.data.getAll();
  await ctx.reply(`Registration complete!\nName: ${data.name}\nAge: ${data.age}`);

  ctx.fsm.clear();
});

// 8. Cancel anytime
bot.command("cancel", async (ctx) => {
  ctx.fsm.clear();
  await ctx.reply("Cancelled");
});

bot.start();

Storage Options

Memory Storage (Development)

Perfect for development and testing. Data is lost when the bot restarts.

bot.use(createFSM({
  storage: "memory"
}));

Redis Storage (Production)

For production environments. Provides persistence and scalability.

import { createFSM, RedisStorage } from "grammy-fsm";
import Redis from "ioredis";

const redis = new Redis({
  host: "localhost",
  port: 6379,
});

bot.use(createFSM({
  storage: new RedisStorage(redis, {
    keyPrefix: "bot:fsm:", // Optional, default: "fsm:"
  }),
  ttl: 3600, // Optional: auto-cleanup after 1 hour
}));

Custom Storage

Implement your own storage backend:

import type { FSMStorage } from "grammy-fsm";

class MyCustomStorage implements FSMStorage {
  async setState(userId: number, state: string): Promise<void> { /* ... */ }
  async getState(userId: number): Promise<string | null> { /* ... */ }
  async setData(userId: number, data: Record<string, any>): Promise<void> { /* ... */ }
  async getData(userId: number): Promise<Record<string, any> | null> { /* ... */ }
  async updateData(userId: number, data: Record<string, any>): Promise<void> { /* ... */ }
  async clear(userId: number): Promise<void> { /* ... */ }
}

bot.use(createFSM({ storage: new MyCustomStorage() }));

Defining States

Using Enums (Recommended)

enum OrderStates {
  ChoosingProduct = "choosing_product",
  ChoosingQuantity = "choosing_quantity",
  ConfirmingOrder = "confirming_order",
}

// Usage
ctx.state.set(OrderStates.ChoosingProduct);
// or shorthand:
ctx.state = OrderStates.ChoosingProduct;

Using Constants

const STATES = {
  MENU: "menu",
  CATALOG: "catalog",
  CART: "cart",
} as const;

// Usage
ctx.state.set(STATES.MENU);
// or shorthand:
ctx.state = STATES.MENU;

Context API

Once you add the FSM plugin, your context provides these methods:

State Management

// Set current state
ctx.state.set("state_name");
ctx.state.set(MyStates.Registration);
// Or use the shorthand:
ctx.state = "state_name";

// Get current state
const state = ctx.state.get(); // Returns string | null

// Check if user has any state
const hasState = ctx.state.has(); // Returns boolean

// Clear state only
ctx.state.clear();
// Or use shorthand:
ctx.state = undefined;
ctx.state = null;

// Clear everything (state and data)
ctx.fsm.clear();

Data Management

// Set all data (overwrites)
ctx.data.setAll({ name: "John", age: 25 });

// Get all data
const data = ctx.data.getAll(); // Returns { name: "John", age: 25 }

// Update data (merges with existing)
ctx.data.update({ city: "NYC" });
// Now: { name: "John", age: 25, city: "NYC" }

// Set individual field (two ways)
ctx.data.set("email", "[email protected]");
ctx.data.email = "[email protected]"; // direct access

// Get individual field (two ways)
const name = ctx.data.get<string>("name"); // "John"
const name2 = ctx.data.name; // direct access

// Delete field
ctx.data.delete("age");

// Clear data only
ctx.data.clear();
// Or use shorthand:
ctx.data = undefined;
ctx.data = null;

Middleware Filters

state() - Single State Filter

Only run handler if user is in specific state:

bot.filter(state("awaiting_name")).on("message:text", async (ctx) => {
  // Only runs when user is in "awaiting_name" state
});

bot.filter(state(MyStates.Registration)).on("message:text", async (ctx) => {
  // Works with enums/constants
});

states() - Multiple States Filter

Run handler if user is in any of the specified states:

bot.filter(states("state1", "state2", "state3")).on("message:text", async (ctx) => {
  // Runs if user is in state1, state2, OR state3
});

bot.on("message",
  states(MyStates.Step1, MyStates.Step2),
  async (ctx) => {
    // Works with enums
  }
);

inAnyState() - Has Any State

Run handler only if user has some state set (any state):

bot.filter(inAnyState()).on("message", async (ctx) => {
  // Runs only if user has a state (not null)
});

noState() - No State Set

Run handler only if user has no state:

bot.filter(noState()).on("message", async (ctx) => {
  // Runs only if user has no state
});

TypeScript Usage

Typed Context

import { Bot, Context } from "grammy";
import type { FSMFlavor } from "grammy-fsm";

// Extend your context with FSM flavor
type MyContext = Context & FSMFlavor;

const bot = new Bot<MyContext>("TOKEN");

Typed Data

interface UserRegistrationData {
  name: string;
  age: number;
  email: string;
}

// Get typed data
const data = ctx.data.getAll<UserRegistrationData>();
console.log(data.name); // TypeScript knows this is a string

// Get typed field
const age = ctx.data.get<number>("age");
console.log(age); // TypeScript knows this is number | undefined

// Direct access is also type-safe if you type-cast
const data2 = ctx.data as unknown as UserRegistrationData;
console.log(data2.name); // TypeScript knows this is a string

API Reference

createFSM(options)

Creates FSM plugin for Grammy.

Options:

  • storage: "memory" | FSMStorage - Storage backend
  • ttl?: Time-to-live in seconds for auto-cleanup (Redis only)
  • onStateChange?: Callback function called on state changes

Context Methods

State Namespace (ctx.state):

  • state.set(state): Set user's state
  • state.get(): Get current state
  • state.has(): Check if user has a state
  • state.clear(): Clear state only
  • state = "value": Shorthand for state.set(value)
  • state = undefined: Shorthand for state.clear()

Data Namespace (ctx.data):

  • data.setAll(data): Set all data (overwrites)
  • data.getAll<T>(): Get all data
  • data.update(data): Update data (merges)
  • data.get<T>(key): Get single field
  • data.set(key, value): Set single field
  • data.delete(key): Delete single field
  • data.clear(): Clear data only
  • data = undefined: Shorthand for data.clear()
  • data.fieldName: Direct field access (get/set)

General (ctx.fsm):

  • clear(): Clear both state and all data

Middleware Filters

  • state(stateName): Filter by single state
  • states(...stateNames): Filter by multiple states
  • inAnyState(): Filter users with any state
  • noState(): Filter users with no state

Best Practices

  1. Always validate user input before transitioning to next state
  2. Clear state when flow is complete or cancelled
  3. Use meaningful state names that describe what you're waiting for
  4. Use enums for better type safety and autocomplete
  5. Set TTL in production to prevent memory/storage leaks
  6. Don't store large data in FSM - use it for temporary flow data only

Examples

For more examples, see the src/example.ts file in the repository.

License

MIT