Skip to content

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:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw

2. Scaffold your project

One command sets up everything — TypeScript, linting, ORM, plugins, Docker — your choice.

sh
npm create gramio@latest ./bot
sh
yarn create gramio ./bot
sh
pnpm create gramio@latest ./bot
sh
bun create gramio@latest ./bot
What's included in the scaffolder?

3. Start developing

sh
cd bot && npm run dev
sh
cd bot && yarn run dev
sh
cd bot && pnpm run dev
sh
cd bot && bun run dev

Your bot is live and hot-reloading. That's it. Now let's see what you can build.


What GramIO looks like

Handle commands

ts
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:

ts
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:

ts
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:

ts
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:

MethodWhat 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:

ts
// 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());
ts
// 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);
    });
ts
// 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:

sh
npm install @gramio/scenes
sh
yarn add @gramio/scenes
sh
pnpm add @gramio/scenes
sh
bun add @gramio/scenes
ts
import { 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:

PluginWhat it does
SessionPer-user state that persists between messages
ScenesMulti-step conversation flows and wizards
I18nInternationalization powered by Fluent
AutoloadAuto-import handlers from the filesystem
Auto-retryAutomatic retry on rate-limit errors
Media-cacheCache uploaded file IDs to avoid re-uploads
Media-groupTreat album updates as a single event
ViewsJSX-based message rendering
PromptWait for the next user message inline
OpenTelemetryDistributed tracing and metrics
SentryError tracking and monitoring

Browse all plugins →


Manual setup

Prefer to wire things up yourself?

sh
npm install gramio
sh
yarn add gramio
sh
pnpm add gramio
sh
bun add gramio

Create src/index.ts:

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();
ts
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:

bash
npx tsx ./src/index.ts
bash
bun ./src/index.ts
bash
deno run --allow-net --allow-env ./src/index.ts