Conversation State

View Source

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.

/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:

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
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:

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:

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 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.