cheat sheet

the most common yaebal patterns on one page.

setup

terminal
pnpm add yaebal
bot.ts
import { createBot } from "yaebal";

export const bot = createBot(process.env.BOT_TOKEN!);
await bot.start();

handlers

handlers.ts
import { createBot } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

bot.command("start", (ctx) => ctx.reply("hi"));
bot.hears(/buy (.+)/, (ctx) => ctx.reply("matched"));
bot.callbackQuery(/^page:(\d+)$/, (ctx) => ctx.answerCallbackQuery());
bot.on("message:text", (ctx) => ctx.text);
bot.on(":photo", (ctx) => ctx.reply("nice photo"));
bot.on("edited_message", (ctx) => ctx.reply("noticed the edit"));
methoduse
command/start, /help, command args
hearstext/caption string or regexp matches
callbackQueryinline button callbacks
on("message:text")filter query with typed narrowing
on(":photo")any update carrying a photo — messages, edits, channel posts
on("edited_message")an edited message, not the original

context enrichment

enrich.ts
import { createBot } from "yaebal";

const loadUser = async (id: number) => ({ id, name: "somebody" });
const bot = createBot(process.env.BOT_TOKEN!)
  .derive(async (ctx) => ({ user: await loadUser(ctx.from!.id) }))
  .decorate({ version: "1.0.0" })
  .on("message:text", (ctx) => {
    ctx.user;
    ctx.version;
    ctx.text;
  });

keyboards and callback data

keyboard.ts
import { createBot, InlineKeyboard, callbackData } from "yaebal";

const vote = callbackData("vote", { id: Number });
const bot = createBot(process.env.BOT_TOKEN!);

bot.command("poll", (ctx) =>
  ctx.reply("pick", {
    reply_markup: new InlineKeyboard()
      .text("yes", vote.pack({ id: 1 }))
      .text("no", vote.pack({ id: 2 }))
      .build(),
  }),
);

.build() is optional — InlineKeyboard/Keyboard implement toJSON(), so passing the builder straight to reply_markup works too. this page always calls it explicitly for clarity.

formatting

format.ts
import { html, format, bold, link, createBot } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);
const user = { name: "Ann" };

bot.command("hi", (ctx) => {
  ctx.send(html`<b>hello</b> ${user.name}`);
  ctx.send(format`${bold("safe entities")} ${link("docs", "https://yaebal.mom")}`);
});

edit, delete, answer

edit.ts
import { createBot } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

bot.callbackQuery(/^refresh$/, async (ctx) => {
  await ctx.answerCallbackQuery(); // stop the client's loading spinner first
  await ctx.editText({ text: "refreshed @ " + new Date().toISOString() });
});

bot.command("cleanup", (ctx) => ctx.delete());

typing a helper outside the chain

helpers.ts
import type { MessageContext } from "yaebal";

// types flow only inside the chain — a helper extracted into its own function
// names its context type explicitly. handler contexts are structural supertypes
// of the per-update class, so ctx passes in with no casts.
async function eatMessage(ctx: MessageContext) {
  try {
    await ctx.delete();
  } catch {
    /* no rights or older than 48h — fine */
  }
}

// needs plugin-added fields? intersect them:
function greet(ctx: MessageContext & { session: { count: number } }) {
  return ctx.reply(`hi #${++ctx.session.count}`);
}

never type a helper as ctx: unknown and poke it with a structural cast ((ctx as { delete?: () => ... }).delete?.()) — the optional call silently no-ops on the wrong update kind, hiding exactly the bugs the named type catches at compile time.

raw api calls

raw.ts
import { createBot } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

// every method is fully typed, generated straight from the Bot API schema —
// reach for this when there's no ctx shortcut yet, or you need a raw result.
await bot.api.call("sendChatAction", { chat_id: 123, action: "typing" });

error handling

errors.ts
import { createBot, TelegramError } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

bot.onError((error, ctx) => {
  if (error instanceof TelegramError && error.parameters?.retry_after) {
    console.warn("rate limited, retry after", error.parameters.retry_after);
    return;
  }
  console.error("update", ctx.update.update_id, "failed:", error);
});

see core concepts and hooks & errors.

media

media.ts
import { createBot, media } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);

bot.command("pic", async (ctx) => {
  await ctx.sendPhoto(media.url("https://example.com/cat.jpg"));
  await ctx.sendDocument(media.path("./report.pdf"));
  await ctx.sendPhoto("AgACAgIAAx...");
});

sessions and i18n

plugins.ts
import { createBot, session, i18n } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!)
  .install(session({ initial: () => ({ count: 0 }) }))
  .install(i18n({ defaultLocale: "en", locales: { en: { hi: "hello" } } }))
  .command("count", (ctx) => ctx.reply(String(++ctx.session.count)));

scenes and prompts

scenes.ts
import { createBot } from "yaebal";
import { defineScene, scenes } from "@yaebal/scenes";
import { prompt } from "@yaebal/prompt";

const echo = defineScene({
  steps: [
    async (ctx) => {
      if (ctx.scene.firstTime) return ctx.send("say something");
      await ctx.send("you said: " + (ctx.text ?? ""));
      return ctx.scene.leave();
    },
  ],
});

const bot = createBot(process.env.BOT_TOKEN!)
  .install(scenes({ echo }))
  .install(prompt())
  .command("echo", (ctx) => ctx.scene.enter("echo"))
  .command("name", (ctx) => ctx.prompt("what's your name?", (ctx) => ctx.reply("hi, " + (ctx.text ?? ""))));

webhooks

worker.ts
import { createBot, webhook } from "yaebal";

const bot = createBot(process.env.BOT_TOKEN!);
export default { fetch: webhook(bot, { secretToken: process.env.WEBHOOK_SECRET }) };

testing

bot.test.ts
import { createTestEnv } from "@yaebal/test";
import { expect, test } from "vitest";
import { bot } from "./bot.js";

test("start", async () => {
  const env = createTestEnv(bot);
  const user = env.createUser({ firstName: "Linia" });

  await user.sendCommand("start");

  expect(env.lastApiCall("sendMessage")?.params?.text).toBeDefined();
});

which package do I need?

needpackage
retry Telegram calls (retry_after, 5xx)@yaebal/again
throttle outgoing sends under Telegram's limits@yaebal/throttle
per-chat state@yaebal/session
multi-step wizards@yaebal/scenes
ask-one-question flows@yaebal/prompt
translated replies@yaebal/i18n
mass messaging with retry and progress@yaebal/broadcast
concurrent long polling@yaebal/runner
in-process bot tests@yaebal/test