A TypeScript Telegram Bot Framework with Grammy, XState-powered Finite State Machines (FSM), Prisma ORM for persistence, and a type-safe builder pattern for defining conversation flows.
Goal: Build bot infrastructure with explicit structure that allows an LLM to build and verify bots from text descriptions, and detect inconsistencies in those descriptions.
- NPM Package: Install as a dependency to any bot project
- Project Scaffolding:
npx telemeister create-botcreates new projects - Grammy Bot Framework: Modern, TypeScript-first Telegram Bot API library
- XState FSM: Compact, maintainable state machines using XState's "states as data" pattern
- Type-Safe State Transitions: Full TypeScript support with strict transition types
- State Machine Configuration: JSON-based state machine definition (
bot.json) - Auto-Generated Types: TypeScript types generated from state machine config
- State Diagram Visualization: Mermaid diagrams (MD + PNG) auto-generated
- Prisma ORM 7.x: Modern database toolkit with driver adapters for SQLite and MySQL
- Single Schema: One Prisma schema works for both SQLite (dev) and MySQL (production)
- Builder Pattern: Fluent API for defining state handlers
- Dual Mode: Supports both Polling and Webhook modes
- CLI Tools: Built-in commands for managing states, transitions, and webhooks
npx telemeister create-bot my-bot
cd my-bot
npm installcp .env.example .env
# Edit .env with your credentialsRequired environment variables:
BOT_TOKEN=your_bot_token # From @BotFather (https://t.me/BotFather)
# Database Configuration
# For SQLite (development):
DATABASE_URL="file:./dev.db"
# For MySQL (production):
# DATABASE_URL="mysql://user:password@localhost:3306/dbname"Generate Prisma Client:
npm run db:generateRun Migrations:
# Development (SQLite)
npm run db:migrate
# Production (MySQL) - after updating DATABASE_URL
npm run db:deployPolling mode (development):
npm run devWebhook mode (production):
# Set webhook URL first
npm run webhook:set -- https://your-domain.com/webhook
# Start in webhook mode
BOT_MODE=webhook npm run devmy-bot/
├── bot.json # State machine configuration (source of truth, gitignored)
├── src/
│ ├── bot-state-types.ts # Auto-generated types (DO NOT EDIT)
│ ├── bot-diagram.md # Auto-generated Mermaid diagram
│ ├── bot-diagram.png # Auto-generated diagram image
│ ├── handlers/ # Your state handlers
│ │ ├── index.ts # Handler imports
│ │ ├── idle/ # Idle state handler
│ │ ├── welcome/ # Welcome state handler
│ │ └── menu/ # Menu state handler
│ └── index.ts # Bot entry point
├── prisma/
│ └── schema.prisma # Database schema
├── .env # Environment variables (gitignored)
└── package.json
The bot.json file is the source of truth for your state machine:
{
"idle": ["welcome"],
"welcome": ["menu"],
"menu": ["welcome", "idle"]
}Each key is a state, and the array contains valid transition targets.
| Command | Description |
|---|---|
telemeister state:add <name> |
Add a new state + create handler |
telemeister state:delete <name> |
Delete a state (with safety checks) |
telemeister state:sync |
Sync types + create missing handlers |
telemeister state:transition:add <from> <to> |
Add a transition |
telemeister state:transition:delete <from> <to> |
Delete a transition |
Or use npm scripts:
npm run state:add -- settings
npm run state:synctelemeister state:add collectEmailThis command:
- Adds
"collectEmail": []tobot.json - Creates
src/handlers/collectEmail/index.tswith a template - Updates
src/handlers/index.tswith the import - Regenerates
src/bot-state-types.ts - Regenerates
src/bot-diagram.mdandsrc/bot-diagram.png
telemeister state:transition:add collectEmail completedThis updates bot.json, regenerates types and diagrams.
Safety checks prevent accidental deletion:
- Cannot delete if handler folder is non-empty
- Cannot delete if state has outgoing transitions
- Cannot delete if state has incoming transitions
# Remove transitions first
telemeister state:transition:delete collectEmail completed
# Then empty the handler folder or move files
rm -rf src/handlers/collectEmail
# Now delete the state
telemeister state:delete collectEmailtelemeister state:syncThis regenerates:
src/bot-state-types.ts- TypeScript types frombot.jsonsrc/bot-diagram.md- Mermaid diagramsrc/bot-diagram.png- PNG image (requires mermaid-cli)- Creates missing handler folders (never overwrites existing)
The src/bot-state-types.ts file is auto-generated:
// Auto-generated by state:sync - DO NOT EDIT
export type AppStates = 'idle' | 'menu' | 'welcome';
export type StateTransitions = {
idle: 'welcome' | void;
menu: 'idle' | 'welcome' | void;
welcome: 'menu' | void;
};
export type IdleTransitions = Promise<StateTransitions['idle']>;
export type MenuTransitions = Promise<StateTransitions['menu']>;
export type WelcomeTransitions = Promise<StateTransitions['welcome']>;Handlers use generated types for strict return type checking:
import { appBuilder, type AppContext } from 'telemeister/core';
import type { MenuTransitions } from './bot-state-types.js';
appBuilder
.forState('menu')
.onEnter(async (context: AppContext): MenuTransitions => {
await context.send('Welcome to menu!');
// Can only return 'idle', 'welcome', or void
})
.onResponse(async (context: AppContext, response): MenuTransitions => {
if (response === 'back') return 'welcome'; // ✅ Valid
if (response === 'exit') return 'idle'; // ✅ Valid
return 'invalid'; // ❌ Type error - not in transitions
});interface BotHandlerContext<TState> {
// User info
userId: number;
telegramId: number;
chatId: number;
currentState: TState;
// Messaging
send: (text: string) => Promise<unknown>;
// Data persistence (per-user)
setData: <T>(key: string, value: T) => void;
getData: <T>(key: string) => T | undefined;
// State transition
transition: (toState: TState) => Promise<void>;
}// Called when entering a state
.onEnter(async (context) => {
await context.send("Welcome!");
// Optionally return a state for immediate transition
return "anotherState";
})
// Called when user sends a message
.onResponse(async (context, response) => {
// Return state name to transition, or void/undefined to stay
if (response === "yes") return "confirmed";
return "cancelled";
})Auto-generated visualizations are updated on every state/transition change:
src/bot-diagram.md:
# Bot State Diagram
```mermaid
stateDiagram-v2
idle --> welcome
welcome --> menu
menu --> welcome
menu --> idle
**`src/bot-diagram.png`:** PNG image rendered by mermaid-cli.
## Database Configuration
### Switching Between SQLite and MySQL
**1. Update `prisma/schema.prisma`:**
```prisma
datasource db {
provider = "sqlite" // Change to "mysql" for production
}
2. Update .env:
# SQLite (development)
DATABASE_URL="file:./dev.db"
# MySQL (production)
DATABASE_URL="mysql://user:password@localhost:3306/dbname"3. Regenerate and migrate:
npm run db:generate
npm run db:migratenpm run db:generate # Generate Prisma Client after schema changes
npm run db:migrate # Create and apply migrations (development)
npm run db:deploy # Apply migrations in production
npm run db:push # Push schema changes without migration files
npm run db:studio # Open Prisma Studio (database GUI)# Set webhook URL
npm run webhook:set -- https://your-domain.com/webhook
# Check webhook info
npm run webhook:info
# Delete webhook (switch back to polling)
npm run webhook:deleteUsers are persisted with:
telegramId- Telegram user IDchatId- Telegram chat IDcurrentState- Current FSM statestateData- JSON data storage for user context (in separateuserInforelation)
model User {
id Int @id @default(autoincrement())
telegramId Int @unique
chatId Int
currentState String @default("idle")
updatedAt DateTime @updatedAt
info UserInfo?
@@index([currentState])
}
model UserInfo {
id Int @id @default(autoincrement())
userId Int @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
stateData String @default("{}")
}User sends message
↓
Load user from DB (by telegramId)
↓
Execute onResponse for current state
↓
Handler returns nextState (or void)
↓
Update DB with new state
↓
Execute onEnter for new state
↓
Send prompt to user
Instead of defining every state in XState:
// Traditional - verbose
states: {
idle: { on: { START: 'welcome' } },
welcome: { on: { NEXT: 'menu' } },
// ... every state
}
// Telemeister - compact
states: {
active: {
on: {
TRANSITION: {
actions: assign({ currentState: ({ event }) => event.toState }),
target: 'active',
reenter: true, // Triggers onEnter
}
}
}
}The actual state value is stored in context.currentState. The bot.json file is the source of truth for valid states and transitions.
This repository contains the Telemeister framework source code.
# Clone and install
git clone <repo>
cd telemeister
npm install
# Build
npm run build
# Run CLI locally
npm run telemeister:state:add -- settings
# Or use tsx directly
npx tsx src/cli/cli.ts state:add settingsnpm run build
npm version patch
npm publish- Grammy: Modern Telegram Bot API framework with excellent TypeScript support
- XState: State machines for complex conversation flows
- Prisma ORM 7.x: Database toolkit with driver adapters
- Driver Adapters: Required adapters for database connections (
@prisma/adapter-better-sqlite3,@prisma/adapter-mariadb) - ESM-Only: Native ES module support
- Generated Client in Source: Better IDE support and file watching
- Driver Adapters: Required adapters for database connections (
- EJS: Template engine for handler generation
- Mermaid CLI: Diagram generation
MIT