Skip to main content
PolyBot’s FSM support lets you guide users through multi-step conversation flows without any manual state management in your routing logic. You annotate handler methods with state predicates; the source generator emits the state checks inline in the router, and IStateMachine gives you a typed, update-scoped API to read and write state from handler code.

Core concepts

A state is an enum value associated with a user’s (or chat’s) identity. Two attributes control which states a handler matches:
  • [State<TEnum>(value)] — restricts the handler to updates where the resolved identity currently holds the given enum value.
  • [NoState<TEnum>] — restricts the handler to updates where the resolved identity has no stored state for TEnum. This is the typical entry point into a flow.
  • Handlers with neither attribute are fully stateless and always execute when reached.

StateKey: choosing the identity dimension

Both [State<TEnum>] and [NoState<TEnum>] accept an optional StateKey parameter that controls which identity from the incoming update is used as the storage key.

Key resolution table

Updates without a resolvable identity (e.g. plain Poll updates) match neither [State] nor [NoState]. They pass through all state-gated handlers silently.

Combining multiple state attributes

Within one handler, attributes sharing the same (TEnum, StateKey) pair are OR-ed — any listed state value matches. Attributes with different pairs are AND-ed — all conditions must hold simultaneously.
Combining [State<TEnum>] and [NoState<TEnum>] for the same enum and StateKey on one handler produces warning CUR011 — the conditions are AND-ed and the handler will never match.

The IStateMachine interface

Inject IStateMachine into any handler to read and write state for the update currently being routed. Keys are resolved from the current update automatically.
GetAsync never throws — it returns null when there is no current update, the key is unresolvable, or no state has been stored. SetAsync and ResetAsync throw InvalidOperationException on unresolvable keys to surface silent state loss as a visible failure.

AdvanceAsync and RollbackAsync

For ordered state enums, StateMachineExtensions provides two convenience transitions that walk one step forward or backward through the enum’s declared values.
The full progression for RegistrationStep { AwaitingName, AwaitingAge, Completed }:

State storage

MemoryStateStorage (default)

AddPolyBotDefaults() registers an in-memory MemoryStateStorage as the IStateStorage singleton. It is keyed by (EnumType, rawKey) pairs, so different enum types never collide for the same identity.
MemoryStateStorage is process-local. It is suitable for development and single-instance bots; for multi-instance or persistent deployments, register a custom IStateStorage implementation.

Custom IStateStorage

Implement IStateStorage to back state in Redis, a database, or any other store. Register it before AddPolyBotRouter() so PolyBot’s try-add semantics pick up your implementation instead of the default.

Complete registration flow example

1

User sends /register with no active state

[NoState<RegistrationStep>] matches. The handler sets state to AwaitingName and asks for the user’s name.
2

User replies with their name

[State<RegistrationStep>(RegistrationStep.AwaitingName)] matches. The handler saves the name, advances state to AwaitingAge, and asks for their age.
3

User replies with their age

[State<RegistrationStep>(RegistrationStep.AwaitingAge)] matches. The handler saves the age, sets state to Completed, and confirms registration.
4

Subsequent messages

Neither [NoState] nor either [State] matches because the state is Completed (and no handler declares that state). Other stateless handlers continue to route normally.

Diagnostics reference

Severity: ErrorThe type argument of [State<TEnum>] or [NoState<TEnum>] is not an enum type. The attribute is ignored and the handler routes unconditionally.
Severity: WarningA handler declares both [State<TEnum>] and [NoState<TEnum>] for the same enum type and StateKey. The conditions are AND-ed, which means the handler can never match any update. Remove one of the conflicting attributes.