Packages
ex_gram
0.66.0
0.67.0
0.66.0
0.65.0
0.64.0
0.63.0
0.62.0
0.61.0
0.60.0
0.58.0
0.57.0
0.56.1
0.56.0
0.55.1
0.55.0
0.54.0
0.53.0
0.52.2
0.52.1
0.52.0
0.51.1
0.51.0
0.50.2
0.50.1
0.50.0
0.41.0
0.40.0
0.34.0
0.33.0
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.15.0
0.14.0
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.2
0.6.1
0.6.0
0.5.0
0.5.0-rc6
0.5.0-rc5
0.5.0-rc4
0.5.0-rc3
0.5.0-rc2
Telegram Bot API low level and framework
Current section
Files
Jump to
Current section
Files
guides/fsm.md
# FSM (Finite State Machine)
Many bots need multi-step conversation flows: registration forms, settings wizards, onboarding sequences. You can model these with pattern matching on `context.extra`, but as flows multiply, managing state transitions, validation, and cleanup becomes tedious and error-prone.
[ExGram.FSM](https://hex.pm/packages/ex_gram_fsm) provides structured conversation state management with:
- **Named flows** with explicitly declared states and valid transitions
- **Runtime transition validation** — catch illegal state jumps early
- **Pluggable storage** — ETS for development, bring your own backend for production
- **Key adapters** — scope state per-user, per-chat, or per-topic
- **Router integration** — automatic `:fsm_flow` and `:fsm_state` filter aliases when used with [ExGram.Router](router.md)
> This guide covers the most common usage. For the full API reference and advanced options, see the [ExGram.FSM HexDocs](https://hexdocs.pm/ex_gram_fsm).
## Installation
Add `ex_gram_fsm` to your dependencies:
```elixir
# mix.exs
def deps do
[
{:ex_gram, "~> 0.65"},
{:ex_gram_fsm, "~> 0.1.0"},
{:jason, ">= 1.0.0"},
{:req, "~> 0.5"}
]
end
```
If you're also using [ExGram.Router](router.md) (recommended), add it too:
```elixir
def deps do
[
{:ex_gram, "~> 0.65"},
{:ex_gram_router, "~> 0.1.0"},
{:ex_gram_fsm, "~> 0.1.0"},
{:jason, ">= 1.0.0"},
{:req, "~> 0.5"}
]
end
```
## Defining Flows
Each conversation flow is a module that declares its states and allowed transitions:
```elixir
defmodule MyBot.RegistrationFlow do
use ExGram.FSM.Flow, name: :registration
defstates do
state :get_name, to: [:get_email]
state :get_email, to: [:done]
state :done, to: []
end
def default_state, do: :get_name
end
```
- **`name:`** — Identifies this flow (used in `start_flow/2` and filters)
- **`defstates`** — Declares valid states and where each can transition to
- **`default_state/0`** — The initial state when the flow starts
The transition graph is validated at compile time and enforced at runtime. If you try `transition(context, :done)` from `:get_name`, the configured policy kicks in (raise, log, or ignore).
## Flow Lifecycle
A flow goes through four stages:
1. **Start** — `start_flow(context, :registration)` activates the flow, sets the default state, and clears any previous data.
2. **Transition** — `transition(context, :get_email)` moves to the next state. The transition is validated against the flow's declared graph.
3. **Accumulate** — `update_data(context, %{name: name})` merges data into the flow's persistent data map. Collect form fields step by step.
4. **End** — `clear_flow(context)` resets everything: no active flow, no state, no data.
## Basic Example (Without Router)
You can use ExGram.FSM with plain `handle/2` pattern matching:
```elixir
defmodule MyBot do
use ExGram.Bot, name: :my_bot, setup_commands: true
use ExGram.FSM,
storage: ExGram.FSM.Storage.ETS,
flows: [MyBot.RegistrationFlow]
command("register", description: "Start registration")
def handle({:command, :register, _}, context) do
context
|> start_flow(:registration)
|> answer("What's your name?")
end
def handle(
{:text, name, _},
%{extra: %{fsm: %ExGram.FSM.State{flow: :registration, state: :get_name}}} = context
) do
context
|> update_data(%{name: name})
|> transition(:get_email)
|> answer("Got it, #{name}! What's your email?")
end
def handle(
{:text, email, _},
%{extra: %{fsm: %ExGram.FSM.State{flow: :registration, state: :get_email}}} = context
) do
%{name: name} = get_data(context)
context
|> update_data(%{email: email})
|> clear_flow()
|> answer("Done! Welcome, #{name} (#{email}).")
end
def handle(_, context), do: context
end
```
This works fine, but the pattern matching on `context.extra.fsm` is verbose. With [ExGram.Router](router.md), you get dedicated filter aliases.
## With ExGram.Router (Recommended)
When `use ExGram.Router` is present in the same module, `use ExGram.FSM` automatically registers three filter aliases: `:fsm_flow`, `:fsm_state`, and `:fsm_in_flow`.
> **Important:** `use ExGram.Router` must come *before* `use ExGram.FSM`.
```elixir
defmodule MyBot do
use ExGram.Bot, name: :my_bot, setup_commands: true
use ExGram.Router
use ExGram.FSM,
storage: ExGram.FSM.Storage.ETS,
flows: [MyBot.RegistrationFlow]
command("register", description: "Start registration")
scope do
filter :command, :register
handle &MyBot.Handlers.start_registration/1
end
scope do
filter :fsm_flow, :registration
scope do
filter :fsm_state, :get_name
filter :text
handle &MyBot.Handlers.got_name/2
end
scope do
filter :fsm_state, :get_email
filter :text
handle &MyBot.Handlers.got_email/2
end
end
scope do
handle &MyBot.Handlers.fallback/1
end
end
```
```elixir
defmodule MyBot.Handlers do
import ExGram.Dsl
def start_registration(context) do
context
|> start_flow(:registration)
|> answer("What's your name?")
end
def got_name({:text, name, _}, context) do
context
|> update_data(%{name: name})
|> transition(:get_email)
|> answer("Got it, #{name}! What's your email?")
end
def got_email({:text, email, _}, context) do
%{name: name} = get_data(context)
context
|> update_data(%{email: email})
|> clear_flow()
|> answer("Registered! Welcome, #{name} (#{email}).")
end
def fallback(context), do: context
end
```
The routing structure makes the flow visible at a glance: the outer scope guards on `:fsm_flow`, and each inner scope matches a specific step plus the expected input type.
### FSM Filter Reference
| Filter | Options | Matches when… |
|--------|---------|---------------|
| `:fsm_flow` | atom or `nil` | Active flow matches the given name (or `nil` for no active flow) |
| `:fsm_state` | atom | Current state matches the given atom |
| `:fsm_state` | `{key, value}` | `fsm_data[key] == value` |
| `:fsm_in_flow` | (none) | Any flow is active |
## Helper Functions
`use ExGram.FSM` imports these functions into your bot module (and they're available in handler modules that import from the bot):
| Function | Description |
|----------|-------------|
| `start_flow(context, flow_name)` | Start a flow — sets default state and clears data |
| `transition(context, state)` | Move to the next state (validated) |
| `set_state(context, state)` | Force-set state, bypassing validation |
| `set_state(context, flow, state)` | Force-set flow + state (escape hatch) |
| `get_flow(context)` | Current active flow name, or `nil` |
| `get_state(context)` | Current step within the active flow, or `nil` |
| `get_data(context)` | FSM data map (never `nil`) |
| `update_data(context, map)` | Merge a map into the FSM data |
| `clear_flow(context)` | Reset: no flow, no state, no data |
All helpers take and return `ExGram.Cnt.t()`, so they work seamlessly in pipelines.
### `transition/2` vs `set_state/2`
- **`transition/2`** validates the move against the flow's declared transitions and applies the `on_invalid_transition` policy if it's not allowed. This is the normal path — use it in your handlers.
- **`set_state/2`** and **`set_state/3`** bypass validation entirely. Use them for admin tools, recovery, or testing.
## Transition Policies
Configure what happens when an invalid transition is attempted:
```elixir
use ExGram.FSM,
storage: ExGram.FSM.Storage.ETS,
flows: [MyBot.RegistrationFlow],
on_invalid_transition: :log # or :raise, :ignore, {Module, :function}
```
| Policy | Behavior |
|--------|----------|
| `:raise` (default) | Raises `ExGram.FSM.TransitionError` |
| `:log` | Logs a warning, returns context unchanged |
| `:ignore` | Silent no-op, returns context unchanged |
| `{Module, :function}` | Calls `Module.function(context, from, to)` for custom handling |
## Storage Backends
The default backend is `ExGram.FSM.Storage.ETS` — in-memory, single-node, and **state is lost on restart**. This is fine for development and simple bots.
For production, implement the `ExGram.FSM.Storage` behaviour:
```elixir
defmodule MyBot.RedisStorage do
@behaviour ExGram.FSM.Storage
@impl true
def init(bot_name, _opts), do: :ok
@impl true
def get_state(bot_name, key), do: # read from Redis
@impl true
def set_state(bot_name, key, %ExGram.FSM.State{} = state), do: # write
@impl true
def get_data(bot_name, key), do: # read data
@impl true
def set_data(bot_name, key, data), do: # write data
@impl true
def update_data(bot_name, key, new_data), do: # merge and write
@impl true
def clear(bot_name, key), do: # delete
end
```
Use it:
```elixir
use ExGram.FSM, storage: MyBot.RedisStorage, flows: [...]
```
Storage is bot-scoped: the `bot_name` argument lets a single backend serve multiple bots without key collisions. The ETS implementation creates one named table per bot.
## Key Adapters
Key adapters control how FSM state is scoped — who shares state with whom:
| Adapter | Key | Use case |
|---------|-----|----------|
| `ExGram.FSM.Key.ChatUser` (default) | `{chat_id, user_id}` | Each user has independent state per chat |
| `ExGram.FSM.Key.User` | `{user_id}` | Same state across all chats (DMs, groups) |
| `ExGram.FSM.Key.Chat` | `{chat_id}` | Shared state for all users in a chat |
| `ExGram.FSM.Key.ChatTopic` | `{chat_id, thread_id}` | Per forum topic, shared |
| `ExGram.FSM.Key.ChatTopicUser` | `{chat_id, thread_id, user_id}` | Per-user per forum topic |
```elixir
# User-scoped: state follows the user everywhere
use ExGram.FSM, key: ExGram.FSM.Key.User, flows: [...]
# Chat-scoped: shared state, e.g. group game sessions
use ExGram.FSM, key: ExGram.FSM.Key.Chat, flows: [...]
```
You can also implement `ExGram.FSM.Key` to define your own scoping strategy.
## Next Steps
- [Router](router.md) - Declarative routing DSL (pairs perfectly with FSM)
- [Middlewares](middlewares.md) - Enrich context before routing and FSM
- [Handling Updates](handling-updates.md) - Understand update types
- [Testing](testing.md) - Test your bot end-to-end