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