Conversation State
View SourceMulti-step bots need to remember what answer is expected next. Nadia keeps
that choice explicit: the handler owns the state machine and reads or writes a
Nadia.SessionStore backend.
This example collects a name and email address. Its complete tested source is
examples/conversation_bot.ex.
/start -> ask for name -> ask for email -> clear the session
\-> /cancel --------> clear the sessionSupervise State Before Polling
The built-in ETS backend must be running before the handler uses it:
children = [
{Nadia.SessionStore.ETS, name: MyApp.BotSessions},
{Nadia.Polling,
handler: MyApp.ConversationBot,
allowed_updates: ["message"],
timeout: 30}
]The handler refers to the same store by backend and process name:
@store {Nadia.SessionStore.ETS, MyApp.BotSessions}Key State Deliberately
For a private conversation, a combined chat and user key prevents state from leaking between users or chats:
case Nadia.SessionStore.chat_user_key(context) do
{:ok, key} -> continue_conversation(context, key)
{:error, :missing_chat_id} -> :ignore
{:error, :missing_user_id} -> :ignore
endReturning :ignore for an update that cannot have a conversation key matters
when using Nadia.Polling: arbitrary return values are failures and will be
retried. Use chat_key/1 for chat-wide state or user_key/1 when the same
state should follow a user across chats.
Advance The State Machine
Start a conversation by writing the next expected step:
with {:ok, _message} <- Nadia.Context.reply(context, "What is your name?"),
:ok <- Nadia.SessionStore.put(@store, key, %{step: :name}) do
:ok
endOn later messages, read the session and handle the current step:
with {:ok, session} <- Nadia.SessionStore.get(@store, key) do
case {session, context.message} do
{%{step: :name}, %{text: name}} when is_binary(name) ->
with {:ok, _message} <-
Nadia.Context.reply(context, "What is your email address?"),
:ok <- Nadia.SessionStore.put(@store, key, %{step: :email, name: name}) do
:ok
end
{%{step: :email, name: name}, %{text: email}} when is_binary(email) ->
with {:ok, _message} <-
Nadia.Context.reply(context, "Thanks #{name}. Received #{email}."),
:ok <- Nadia.SessionStore.delete(@store, key) do
:ok
end
_ ->
:ignore
end
endTreat prompts and side effects as repeatable. Polling advances its offset only after a successful handler result, and webhook providers may redeliver when a response is lost. A handler can therefore see an update more than once.
Telegram sends and session mutations cannot share one transaction. This
learning example replies before changing the step so a failed send does not
misinterpret the same update after retry; a lost response can still produce a
duplicate prompt. Durable business workflows need application-level
idempotency and often a transactional outbox. The database session example
shows the durable shape: record {bot_ref, update_id}, commit the session or
business-state change, and enqueue outbox intent in one application database
transaction, then send Telegram requests from a worker after commit.
Choose A Production Backend
Nadia.SessionStore.ETS is local, in-memory state. It disappears on restart
and is not shared between nodes. It is suitable for learning, development, and
simple single-node bots where losing a conversation is acceptable.
Implement the Nadia.SessionStore behaviour with application storage when
state must survive deploys, be shared by several nodes, or participate in a
larger transaction. Store only conversational progress in a session; durable
business records belong in the application's primary database.
See Persistent Session Backends for the complete
backend contract, the tested DETS example in examples/disk_session_store.ex,
and the tested database boundary in examples/database_session_store.ex. The
database example covers optimistic conflict retries, processed-update
idempotency markers, and transactional outbox intent; use that pattern when
state changes and durable side effects must commit together.