Get Started
Build your first Telegram bot in under a minute — type-safe, multi-runtime, with a rich plugin ecosystem.
1. Get your bot token
Open @BotFather in Telegram, send /newbot, and follow the prompts. You'll get a token like:
110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw2. Scaffold your project
One command sets up everything — TypeScript, linting, ORM, plugins, Docker — your choice.
npm create gramio@latest ./botyarn create gramio ./botpnpm create gramio@latest ./botbun create gramio@latest ./botWhat's included in the scaffolder?
- ORMs — Prisma, Drizzle
- Linters — Biome, ESLint (with relevant plugins auto-configured)
- Official plugins — Scenes, Session, I18n, Autoload, Prompt, Auto-retry, Media-cache, Media-group
- Other — Dockerfile + docker-compose, Husky git hooks, Jobify (BullMQ wrapper), GramIO storages
3. Start developing
cd bot && npm run devcd bot && yarn run devcd bot && pnpm run devcd bot && bun run devYour bot is live and hot-reloading. That's it. Now let's see what you can build.
What GramIO looks like
Handle commands
import { Bot } from "gramio";
const bot = new Bot(process.env.BOT_TOKEN as string)
.command("start", (ctx) => ctx.send("Hello! 👋"))
.command("help", (ctx) => ctx.send("Here's what I can do..."))
.onStart(({ info }) => console.log(`Running as @${info.username}`));
bot.start();Format messages
No parse_mode — GramIO uses tagged template literals that produce proper MessageEntity objects automatically:
import { Bot, format, bold, italic, link, code } from "gramio";
const bot = new Bot(process.env.BOT_TOKEN as string)
.command("start", (ctx) =>
ctx.send(
format`${bold`Welcome to the bot!`}
Check out ${link("GramIO", "https://gramio.dev")} — ${italic("type-safe all the way down")}.`
)
);
bot.start();Build keyboards
Fluent chainable API for inline and reply keyboards:
import { Bot, InlineKeyboard } from "gramio";
const bot = new Bot(process.env.BOT_TOKEN as string)
.command("menu", (ctx) =>
ctx.send("What would you like to do?", {
reply_markup: new InlineKeyboard()
.text("About", "about")
.url("GitHub", "https://github.com/gramiojs/gramio")
.row()
.text("Settings ⚙️", "settings"),
})
);
bot.start();Inject type-safe context with derive
Enrich every handler with your own data — no casting, fully typed:
import { Bot } from "gramio";
import { db } from "./db";
const bot = new Bot(process.env.BOT_TOKEN as string)
.derive("message", async (ctx) => ({
user: await db.getUser(ctx.from!.id),
}))
.on("message", (ctx) => {
ctx.user;
//
//
//
//
return ctx.send(`Hi, ${ctx.user.name}!`);
});
bot.start();Middleware pipeline with Composer
Bot extends Composer — a chainable type-safe middleware pipeline. Every method enriches the context and returns the updated type, so the chain is always fully typed.
Composer also exists as a standalone class you can import and use independently of Bot. This matters a lot once your project grows:
| Method | What it does |
|---|---|
use(ctx, next) | Raw middleware — call next() to continue |
derive(fn) | Async per-request context enrichment |
decorate(obj) | Static enrichment at startup — zero per-request overhead |
guard(fn) | Only continue if predicate returns true |
on(event, fn) | Handle a specific update type |
extend(composer) | Merge another composer in — inheriting its full types |
Production pattern: shared plugin composer
In a real project, register all your plugins once in a shared Composer, then extend it in every feature file. Each handler file becomes a plain Composer — no Bot import, no token, fully testable:
// src/plugins/index.ts
import { Composer } from "gramio";
import { scenes } from "@gramio/scenes";
import { session } from "@gramio/session";
import { greetingScene } from "../scenes/greeting.ts";
export const composer = new Composer()
.extend(scenes([greetingScene]))
.extend(session());// src/features/start.ts
import { Composer } from "gramio";
import { composer } from "../plugins/index.ts";
export const startFeature = new Composer()
.extend(composer) // ← inherits all plugin types
.command("start", (ctx) => {
ctx.scene; // ✅ fully typed — no Bot, no token needed
return ctx.scene.enter(greetingScene);
});// src/index.ts
import { Bot } from "gramio";
import { composer } from "./plugins/index.ts";
import { startFeature } from "./features/start.ts";
const bot = new Bot(process.env.BOT_TOKEN as string)
.extend(composer) // plugins
.extend(startFeature); // feature handlers
bot.start();Why this works
Composer carries its type through every .extend(). When startFeature extends composer, TypeScript sees all the properties that plugins added — ctx.scene, ctx.session, etc. — without any casting or manual type annotation.
Extend with plugins
Plugins are installed with .extend(). They can add new context properties, register handlers, and hook into the lifecycle — all fully typed.
Here's how to add Scenes for multi-step conversation flows:
npm install @gramio/scenesyarn add @gramio/scenespnpm add @gramio/scenesbun add @gramio/scenesimport { Bot } from "gramio";
import { scenes, Scene } from "@gramio/scenes";
const registrationScene = new Scene("registration")
.step("message", (ctx) => {
if (ctx.scene.step.firstTime) return ctx.send("What's your name?");
return ctx.scene.update({ name: ctx.text });
})
.step("message", (ctx) => {
ctx.scene.state;
//
//
//
//
return ctx.send(`Nice to meet you, ${ctx.scene.state.name}!`);
});
const bot = new Bot(process.env.BOT_TOKEN as string)
.extend(scenes([registrationScene]))
.command("start", (ctx) => ctx.scene.enter(registrationScene));
bot.start();Each .step() is triggered by the next matching update from the same user. State is persisted across steps and fully typed.
Official plugins
Extend your bot with first-party plugins that integrate seamlessly:
| Plugin | What it does |
|---|---|
| Session | Per-user state that persists between messages |
| Scenes | Multi-step conversation flows and wizards |
| I18n | Internationalization powered by Fluent |
| Autoload | Auto-import handlers from the filesystem |
| Auto-retry | Automatic retry on rate-limit errors |
| Media-cache | Cache uploaded file IDs to avoid re-uploads |
| Media-group | Treat album updates as a single event |
| Views | JSX-based message rendering |
| Prompt | Wait for the next user message inline |
| OpenTelemetry | Distributed tracing and metrics |
| Sentry | Error tracking and monitoring |
Manual setup
Prefer to wire things up yourself?
npm install gramioyarn add gramiopnpm add gramiobun add gramioCreate src/index.ts:
import { Bot } from "gramio";
const bot = new Bot(process.env.BOT_TOKEN as string)
.command("start", (ctx) => ctx.send("Hi! 👋"))
.onStart(console.log);
bot.start();import { Bot } from "jsr:@gramio/core";
const bot = new Bot(process.env.BOT_TOKEN as string)
.command("start", (ctx) => ctx.send("Hi! 👋"))
.onStart(console.log);
bot.start();And run it:
npx tsx ./src/index.tsbun ./src/index.tsdeno run --allow-net --allow-env ./src/index.ts