Skip to main content
Inline awaits let a handler pause mid-conversation and resume when a matching update arrives — all without blocking the polling thread or holding any connections open. While a handler is suspended, the rest of the routing pipeline continues processing other users’ updates normally.

How it works

Inject IUpdateAwaiter into a handler, call one of the generated WaitFor{UpdateType}Async() extension methods, chain any filter conditions, and call a terminal method that registers the await and returns Task<T?>. The handler suspends at that point; the generated router resumes it when a future update satisfies the declared conditions.
The Result returned by a suspended-then-resumed handler does not affect routing. Once a handler suspends, the update that triggered the suspension is already considered handled.

Builder chain

Entry point — WaitForMessageAsync() and friends

The source generator emits one WaitFor{UpdateType}Async() extension method per Telegram.Bot update type. The most common is WaitForMessageAsync(), which matches any message-shaped payload (Message, EditedMessage, ChannelPost, EditedChannelPost, BusinessMessage, EditedBusinessMessage, GuestMessage).
An optional timeout overload causes the await to complete with null on expiry:

TextMatches(pattern) — regex filter

Restricts the await to updates whose message Text (falling back to Caption) matches the regex. Multiple .TextMatches() calls are AND-ed.
Using TextMatches on an await whose target DTO is not a message-shaped payload (e.g. WaitForCallbackQueryAsync) produces warning CUR014 and no implicit await branch is generated for that site.

With{FilterName}() — generated filter conditions

The generator emits one With{FilterName}() extension method per discovered concrete IUpdateFilter implementation. These are the same filters used with [XxxFilter] handler attributes.
Referencing an unknown filter name produces warning CUR015. The filter check will not be emitted into the await branch.

Where(static lambda) — inline predicate

Splices a static lambda directly into the generated router branch. The predicate is evaluated before the update is claimed by the awaiter.
Constraints on Where lambdas (violation → CUR016, predicate not emitted):
  • Must be declared static (prevents captures; the static modifier makes accidental captures a compile error)
  • Must be non-async
  • Must be expression-bodied (no block body)
  • Body may only reference the lambda parameter and literals — no external variables or members

Terminal methods — ByUserId, ByChatId, ByUserInChat

The terminal method registers the await and determines which identity dimension to key on.

Identity and lifetime

Each await is single-shot: it is removed once claimed or once the timeout expires. Two await sites with different declarative signatures (different payload types, TextMatches patterns, filter types, or Where flag) on the same identity can coexist — the router validates the signature before claiming. Awaits are keyed by a numeric identity (user/chat id pair probed most-specific-first). The registration’s signature is held in per-site static constant arrays compared by reference and ordinal equality, so a different await site cannot accidentally claim another site’s pending await.

Non-matching updates fall through

While a handler is suspended, updates that do not satisfy the await’s conditions fall through to normal routing. Other handlers continue working for the same user during the wait.

Complete multi-step conversation example

1

User sends /onboard

The handler sends a greeting and registers an await keyed by the user’s id, waiting for a text message.
2

User sends their name

The await claims the update, the handler resumes, stores the name, and registers a second await.
3

User sends their city

The second await is claimed, the handler finalizes the onboarding flow and returns.

Diagnostics reference

Severity: WarningA handler parameter is typed as IUpdateAwaiter but the source generator found no WaitFor*Async call on it in the method body. The handler will run to completion inline. If the handler does call WaitFor*Async through a wrapper method, the await will deadlock the sequential poller — call WaitFor*Async directly on the IUpdateAwaiter parameter instead.
Severity: WarningTextMatches is chained onto an await whose target DTO is not message-shaped. The text-match condition can never be satisfied and no await branch is emitted for that site.
Severity: WarningAn await chain references a filter type that could not be resolved during generation. The filter check will not be emitted into the await branch.
Severity: WarningA Where predicate does not satisfy all static-lambda constraints (must be static, non-async, expression-bodied, and reference only the parameter). The predicate is not emitted into the await branch and is never evaluated.