Current section
Files
Jump to
Current section
Files
guides/examples/conversation-state.md
# Conversation State
Multi-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`](https://github.com/zhyu/nadia/blob/master/examples/conversation_bot.ex).
```text
/start -> ask for name -> ask for email -> clear the session
\-> /cancel --------> clear the session
```
## Supervise State Before Polling
The built-in ETS backend must be running before the handler uses it:
```elixir
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:
```elixir
@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:
```elixir
case Nadia.SessionStore.chat_user_key(context) do
{:ok, key} -> continue_conversation(context, key)
{:error, :missing_chat_id} -> :ignore
{:error, :missing_user_id} -> :ignore
end
```
Returning `: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:
```elixir
with {:ok, _message} <- Nadia.Context.reply(context, "What is your name?"),
:ok <- Nadia.SessionStore.put(@store, key, %{step: :name}) do
:ok
end
```
On later messages, read the session and handle the current step:
```elixir
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
end
```
Treat 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](persistent-sessions.md) 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.