When your bot receives an update from Telegram, grammY wraps it in a context object . Context objects are passed to all middleware and provide convenient shortcuts for working with the Telegram Bot API.
What is a Context?
A context object does two main things:
Holds the update - Access the raw update via ctx.update
Provides API shortcuts - Call Bot API methods with pre-filled parameters
import { Context } from 'grammy'
bot . on ( 'message' , ( ctx : Context ) => {
// Access the update
console . log ( ctx . update )
// Access the message
console . log ( ctx . message )
// Reply to the message
await ctx . reply ( 'Got your message!' )
})
Context Properties
Every context object has these core properties:
The complete update object from Telegram
The Bot API instance for making API calls
Information about your bot
match
string | RegExpMatchArray | undefined
Populated by methods like hears(), command(), and callbackQuery() with matched content
Update Shortcuts
Instead of accessing ctx.update.message, use convenient shortcuts:
Direct Update Properties
bot . on ( 'message' , ( ctx ) => {
// All of these are shortcuts to ctx.update.*
ctx . message // ctx.update.message
ctx . editedMessage // ctx.update.edited_message
ctx . channelPost // ctx.update.channel_post
ctx . editedChannelPost // ctx.update.edited_channel_post
ctx . inlineQuery // ctx.update.inline_query
ctx . callbackQuery // ctx.update.callback_query
ctx . shippingQuery // ctx.update.shipping_query
ctx . preCheckoutQuery // ctx.update.pre_checkout_query
ctx . myChatMember // ctx.update.my_chat_member
ctx . chatMember // ctx.update.chat_member
ctx . chatJoinRequest // ctx.update.chat_join_request
ctx . messageReaction // ctx.update.message_reaction
ctx . messageReactionCount // ctx.update.message_reaction_count
ctx . chatBoost // ctx.update.chat_boost
ctx . removedChatBoost // ctx.update.removed_chat_boost
// Business updates
ctx . businessConnection // ctx.update.business_connection
ctx . businessMessage // ctx.update.business_message
ctx . editedBusinessMessage // ctx.update.edited_business_message
ctx . deletedBusinessMessages // ctx.update.deleted_business_messages
// Other updates
ctx . poll // ctx.update.poll
ctx . pollAnswer // ctx.update.poll_answer
ctx . chosenInlineResult // ctx.update.chosen_inline_result
ctx . purchasedPaidMedia // ctx.update.purchased_paid_media
})
Aggregation Shortcuts
These shortcuts intelligently aggregate data from multiple possible sources:
ctx.msg
Get the message from wherever it appears in the update:
bot . on ( 'message' , ( ctx ) => {
// ctx.msg works for all of these:
// - ctx.message
// - ctx.editedMessage
// - ctx.channelPost
// - ctx.editedChannelPost
// - ctx.businessMessage
// - ctx.editedBusinessMessage
// - ctx.callbackQuery.message
const msg = ctx . msg
console . log ( msg ?. text )
})
ctx.chat
Get the chat from wherever possible:
bot . on ( 'message' , ( ctx ) => {
// Available from:
// - ctx.msg?.chat
// - ctx.myChatMember?.chat
// - ctx.chatMember?.chat
// - ctx.chatJoinRequest?.chat
// - ctx.messageReaction?.chat
// - ctx.chatBoost?.chat
// - etc.
const chat = ctx . chat
console . log ( `Chat ID: ${ chat ?. id } ` )
console . log ( `Chat type: ${ chat ?. type } ` )
})
ctx.from
Get the user who triggered the update:
bot . on ( 'message' , ( ctx ) => {
// Available from:
// - ctx.msg?.from
// - ctx.callbackQuery?.from
// - ctx.inlineQuery?.from
// - ctx.chosenInlineResult?.from
// - ctx.myChatMember?.from
// - ctx.chatMember?.from
// - ctx.messageReaction?.user
// - etc.
const user = ctx . from
console . log ( `User: ${ user ?. first_name } (@ ${ user ?. username } )` )
})
Other Aggregations
The sender chat from ctx.msg?.sender_chat
Message ID from ctx.msg, ctx.messageReaction, or ctx.messageReactionCount
Chat ID from ctx.chat?.id or ctx.businessConnection?.user_chat_id
Inline message ID from ctx.callbackQuery or ctx.chosenInlineResult
Business connection ID from various business-related updates
API Shortcuts
Context provides convenient methods that are shortcuts to ctx.api.* with pre-filled parameters.
Sending Messages
ctx.reply()
Reply to the current chat:
bot . on ( 'message:text' , async ( ctx ) => {
await ctx . reply ( 'You said: ' + ctx . message . text )
// Equivalent to:
await ctx . api . sendMessage (
ctx . chat . id ,
'You said: ' + ctx . message . text ,
{ message_thread_id: ctx . message . message_thread_id }
)
})
Simple Reply
Reply with Options
Reply to a Specific Message
await ctx . reply ( 'Hello!' )
ctx.replyWithPhoto()
await ctx . replyWithPhoto (
'https://grammy.dev/images/grammY.png' ,
{ caption: 'grammY logo' }
)
Other Reply Methods
ctx.replyWithAudio() - Send audio files
ctx.replyWithDocument() - Send documents
ctx.replyWithVideo() - Send videos
ctx.replyWithAnimation() - Send animations/GIFs
ctx.replyWithVoice() - Send voice messages
ctx.replyWithVideoNote() - Send video notes
ctx.replyWithSticker() - Send stickers
ctx.replyWithDice() - Send dice
ctx.replyWithPoll() - Send polls
ctx.replyWithLocation() - Send locations
ctx.replyWithVenue() - Send venues
ctx.replyWithContact() - Send contacts
ctx.replyWithInvoice() - Send invoices
ctx.replyWithGame() - Send games
Forwarding and Copying
ctx.forwardMessage()
// Forward the current message to another chat
await ctx . forwardMessage ( targetChatId )
ctx.copyMessage()
// Copy the current message to another chat (without forward header)
await ctx . copyMessage ( targetChatId )
Message Editing
bot . on ( 'callback_query:data' , async ( ctx ) => {
// Edit the message that has the button
await ctx . editMessageText ( 'Updated text!' )
// Edit other properties
await ctx . editMessageCaption ( 'New caption' )
await ctx . editMessageMedia ({ type: 'photo' , media: 'url' })
await ctx . editMessageReplyMarkup ({ inline_keyboard: [[]] })
// Delete the message
await ctx . deleteMessage ()
})
Callback Query Handling
bot . on ( 'callback_query' , async ( ctx ) => {
// Must always answer callback queries!
await ctx . answerCallbackQuery ()
// With an alert
await ctx . answerCallbackQuery ({
text: 'Button clicked!' ,
show_alert: true
})
})
User and Chat Actions
// Show typing indicator
await ctx . replyWithChatAction ( 'typing' )
// Ban a user
await ctx . banChatMember ( userId )
// Unban a user
await ctx . unbanChatMember ( userId )
// Set chat photo
await ctx . setChatPhoto ({ source: '/path/to/photo.jpg' })
// Leave chat
await ctx . leaveChat ()
Helper Methods
ctx.entities()
Extract entities from messages:
bot . on ( 'message:text' , ( ctx ) => {
// Get all entities with their text
const entities = ctx . entities ()
entities . forEach ( entity => {
console . log ( ` ${ entity . type } : ${ entity . text } ` )
})
// Get specific entity types
const urls = ctx . entities ( 'url' )
const mentions = ctx . entities ([ 'mention' , 'text_mention' ])
})
ctx.reactions()
Analyze reaction updates:
bot . on ( 'message_reaction' , ( ctx ) => {
const r = ctx . reactions ()
console . log ( 'Current emoji:' , r . emoji )
console . log ( 'Added emoji:' , r . emojiAdded )
console . log ( 'Removed emoji:' , r . emojiRemoved )
console . log ( 'Kept emoji:' , r . emojiKept )
console . log ( 'Custom emoji:' , r . customEmoji )
console . log ( 'Has paid reaction:' , r . paid )
console . log ( 'Just added paid:' , r . paidAdded )
})
Context Predicates
Test if a context matches certain conditions:
ctx.has()
Check if context matches a filter query:
bot . use (( ctx ) => {
if ( ctx . has ( ':text' )) {
// ctx is now typed to have text
console . log ( ctx . msg . text )
}
if ( ctx . has ( 'message:photo' )) {
// ctx.message and ctx.message.photo are guaranteed
console . log ( 'Photo received' )
}
})
Specialized Predicates
if ( ctx . hasText ( 'hello' )) {
// Text is exactly 'hello'
}
if ( ctx . hasText ( / ^ hello/ i )) {
// Text matches regex
console . log ( ctx . match ) // RegExpMatchArray
}
if ( ctx . hasCommand ( 'start' )) {
// Has /start command
console . log ( ctx . match ) // Text after command
}
if ( ctx . hasChatType ( 'private' )) {
// Is a private chat
}
if ( ctx . hasCallbackQuery ( 'button-data' )) {
// Callback query data matches
}
if ( ctx . hasReaction ( '๐' )) {
// New thumbs up reaction
}
Static Predicates
Generate reusable predicate functions:
import { Context } from 'grammy'
// Create predicates
const isAdmin = Context . has . filterQuery ( ':from:id' )
const hasPhoto = Context . has . filterQuery ( 'message:photo' )
const hasUrl = Context . has . text ( /https ? : \/\/ / )
// Use in middleware
bot . use (( ctx ) => {
if ( hasPhoto ( ctx )) {
// ctx is typed correctly
console . log ( 'Photo size:' , ctx . message . photo . length )
}
})
Custom Context
Extend the context with your own properties:
import { Context } from 'grammy'
interface MyContext extends Context {
session : {
count : number
username ?: string
}
}
const bot = new Bot < MyContext >( 'TOKEN' )
// Middleware can now access custom properties
bot . use (( ctx ) => {
ctx . session . count ++
})
Custom Context Constructor
Create a custom context class:
class MyContext extends Context {
constructor (
update : Update ,
api : Api ,
me : UserFromGetMe
) {
super ( update , api , me )
}
// Add custom methods
replyWithError ( message : string ) {
return this . reply ( `โ Error: ${ message } ` )
}
get userName () : string {
return this . from ?. first_name ?? 'Unknown'
}
}
// Pass to Bot constructor
const bot = new Bot ( 'TOKEN' , {
ContextConstructor: MyContext
})
bot . on ( 'message' , ( ctx ) => {
await ctx . replyWithError ( 'Something went wrong' )
console . log ( `Message from ${ ctx . userName } ` )
})
Type Narrowing
Filter queries automatically narrow context types:
// ctx.message is guaranteed to exist and have text
bot . on ( 'message:text' , ( ctx ) => {
const text : string = ctx . message . text
// ^-- Type is string, not string | undefined
})
// Multiple properties guaranteed
bot . on ( 'message:photo' , ( ctx ) => {
const photos = ctx . message . photo // Photo[]
const caption = ctx . message . caption // string | undefined
})
// Nested properties
bot . on ( 'message:entities:url' , ( ctx ) => {
const entities = ctx . message . entities // MessageEntity[]
// At least one entity is type 'url'
})
Context in Action
Hereโs a complete example showing various context features:
import { Bot } from 'grammy'
const bot = new Bot ( 'TOKEN' )
bot . command ( 'start' , async ( ctx ) => {
const user = ctx . from
await ctx . reply (
`Welcome, ${ user ?. first_name } ! \n\n ` +
`Your ID: ${ user ?. id } \n ` +
`Chat ID: ${ ctx . chat ?. id } \n ` +
`Message ID: ${ ctx . msgId } `
)
})
bot . on ( 'message:text' , async ( ctx ) => {
// Extract entities
const urls = ctx . entities ( 'url' )
if ( urls . length > 0 ) {
await ctx . reply (
`Found ${ urls . length } URL(s): \n ` +
urls . map ( e => e . text ). join ( ' \n ' )
)
}
})
bot . on ( 'callback_query:data' , async ( ctx ) => {
await ctx . answerCallbackQuery ( 'Processing...' )
const data = ctx . callbackQuery . data
if ( data === 'delete' ) {
await ctx . deleteMessage ()
} else {
await ctx . editMessageText ( `You clicked: ${ data } ` )
}
})
bot . on ( 'message_reaction' , async ( ctx ) => {
const r = ctx . reactions ()
if ( r . emojiAdded . includes ( '๐' )) {
await ctx . reply ( 'Thanks for the thumbs up!' )
}
})
await bot . start ()
Best Practices
Use Shortcuts Prefer context shortcuts over direct update access: // Good
const text = ctx . message ?. text
// Better
const text = ctx . msg ?. text
Type Safety with Filter Queries Use filter queries for automatic type narrowing: // No type guards needed
bot . on ( 'message:text' , ( ctx ) => {
const text = ctx . message . text // Type is string
})
Donโt Mutate Context The context object should be treated as read-only except for the match property and any custom properties you add.