context

one object wraps each update, exposes typed accessors, and grows new properties as the chain enriches it.

the base context

every update is wrapped in a Context. it holds the raw update and a detected updateType, and derives the common shapes through getters — so ctx.message already resolves message / edited_message / channel_post, and ctx.text falls back from text to caption.

handler.ts
bot.on("message:text", (ctx) => {
  ctx.update;      // the raw Update
  ctx.updateType;  // "message" | "callback_query" | …
  ctx.message;     // message ?? edited_message ?? channel_post
  ctx.from;        // User | undefined
  ctx.chat;        // Chat | undefined
  ctx.text;        // message.text ?? message.caption
  ctx.is("callback_query"); // puregram-style narrowing check
});

sending from the context

the context carries a handful of hand-written shortcuts that infer the chat from the current update. send accepts a plain string or a format result; reply sets reply_parameters for you; the media shortcuts accept a MediaSource or a raw file_id/url string.

handler.ts
bot.on("message:text", async (ctx) => {
  await ctx.send("hi");                       // to the current chat
  await ctx.reply("yo");                      // reply_parameters set for you
  await ctx.sendPhoto("AgAC…");               // file_id or url string
  await ctx.sendDocument(media.path("./a.pdf"));
});

bot.on("callback_query", (ctx) =>
  ctx.answerCallbackQuery({ text: "got it" }), // no-op if no query
);
guard rails. send/sendPhoto/sendDocument reject if the update has no chat, and answerCallbackQuery resolves to false when there is no callback query — so they're safe to call unconditionally.

filter queries

grammY-style L1:L2:L3 queries route on the update. the first segment must match ctx.updateType; each following segment is a field that must be present.

filters.ts
bot.on("message:text", (ctx) => ctx.text);        // L1:L2
bot.on("message:caption", (ctx) => ctx.text);     // caption also fills .text
bot.on("callback_query:data", (ctx) => ctx.callbackQuery);
bot.on("message:entities", (ctx) => ctx.entities);
bot.on(":photo", (ctx) => { /* any update with a message.photo */ });
matchQuery
// matchQuery splits "message:text" into head + fields and checks each:
//   head  → must equal ctx.updateType
//   text  → ctx.text is a non-empty string
//   data  → ctx.callbackQuery?.data is set
//   entities → ctx.message?.entities has length
//   <other> → truthy on ctx.message[field] (e.g. photo, document, sticker)

how Filtered narrows

the same query that routes also narrows the context type. Filtered<C, Q> is a conditional type: for the queries it knows, it intersects the matching field onto the context so your handler sees it as non-optional.

filtered.ts
// Filtered<C, Q> — how a query narrows the context type:
//   "…:text" | "…:caption"     →  C & { text: string }
//   "…:data" | "callback_query" →  C & { callbackQuery: CallbackQuery }
//   "…:entities…"               →  C & { entities: MessageEntity[] }
//   anything else               →  C  (unchanged)

bot.on("message:text", (ctx) => {
  ctx.text;          // string, not string | undefined
});
bot.on("callback_query:data", (ctx) => {
  ctx.callbackQuery; // CallbackQuery, guaranteed present
});

derive / decorate accumulation

on top of filter-query narrowing, derive and decorate add their own properties to the context type, and those carry downstream to every handler after them in the chain.

enrich.ts
bot
  .derive(async (ctx) => ({ user: await db.find(ctx.from!.id) })) // per-request
  .decorate({ appVersion: "1.0.0" })                              // static
  .on("message:text", (ctx) => {
    ctx.user;        // ✅ from derive
    ctx.appVersion;  // ✅ from decorate
    ctx.text;        // ✅ from the filter query
  });

generated shortcuts

the base Context shown here is intentionally small. the much larger set of per-update context classes — with API-method shortcuts generated from the Bot API schema — is the autogen layer.

  • contexts — the auto-generated context layer (the killer feature)