Runtime helper functions for FSM flow and state management.
All functions take and return ExGram.Cnt.t() for pipeline compatibility.
These are automatically imported into your bot module when you call
use ExGram.FSM.
Context keys
The helpers read FSM config from context.extra keys set by the middleware:
| Key | Type | Description |
|---|---|---|
context.extra.fsm | %ExGram.FSM.State{} | Current flow + state + data |
context.extra.fsm_key | term() | Storage key (shape depends on configured key module) |
context.extra.fsm_storage | module | Storage backend module |
context.extra.fsm_flows | %{atom => module} | Registered flow modules map |
context.extra.fsm_on_invalid_transition | atom or tuple | Invalid transition policy |
Flow lifecycle
# 1. Start a flow (sets flow name, default state, clears data)
context |> start_flow(:registration)
# 2. Transition through states (validated against the active flow)
context |> transition(:get_email)
# 3. Accumulate data
context |> update_data(%{name: "Alice"})
# 4. End the flow (resets to no-flow state)
context |> clear_flow()
Summary
Functions
Resets the FSM to a clean state: no active flow, no state, no data.
Returns the current FSM data map from context.
Returns the current active flow name atom from context.
Returns the current FSM state atom (step within the active flow) from context.
Unconditionally sets the FSM state within the currently active flow.
Force sets a flow and state, bypassing all conflict and validation checks.
Starts a named flow for the current user/chat.
Sets the FSM state with transition validation against the active flow.
Merges a map into the current FSM data, persisting the result.
Functions
@spec clear_flow(ExGram.Cnt.t()) :: ExGram.Cnt.t()
Resets the FSM to a clean state: no active flow, no state, no data.
Calls storage.clear/1 to remove the user's FSM record entirely.
Example
def handle({:command, :cancel, _}, context) do
context
|> clear_flow()
|> answer("Cancelled. Send /start to begin again.")
end
@spec get_data(ExGram.Cnt.t()) :: map()
Returns the current FSM data map from context.
Never returns nil — returns %{} if no FSM state exists.
Example
def handle({:command, :status, _}, context) do
data = get_data(context)
answer(context, "Your data: #{inspect(data)}")
end
@spec get_flow(ExGram.Cnt.t()) :: atom() | nil
Returns the current active flow name atom from context.
Returns nil if no flow is active or if the FSM middleware didn't run.
Example
def handle({:command, :status, _}, context) do
flow = get_flow(context)
answer(context, "Current flow: #{inspect(flow)}")
end
@spec get_state(ExGram.Cnt.t()) :: atom() | nil
Returns the current FSM state atom (step within the active flow) from context.
Returns nil if no state is set or if the FSM middleware didn't run.
Example
def handle({:command, :status, _}, context) do
state = get_state(context)
answer(context, "Current step: #{inspect(state)}")
end
@spec set_state(ExGram.Cnt.t(), atom()) :: ExGram.Cnt.t()
Unconditionally sets the FSM state within the currently active flow.
Requires an active flow. If no flow is active, the on_invalid_transition
policy is applied (with from: nil, to: new_state).
This bypasses transition validation — use transition/2 for the normal path.
Use cases:
- Resetting to the flow's first step
- Admin override within a flow
- Recovery from an error state within a flow
Example
def handle({:command, :restart_flow, _}, context) do
context
|> set_state(:get_name)
|> answer("Let's start over. What's your name?")
end
@spec set_state(ExGram.Cnt.t(), atom(), atom()) :: ExGram.Cnt.t()
Force sets a flow and state, bypassing all conflict and validation checks.
This is the escape hatch — it always succeeds regardless of the current flow. The data map is preserved as-is. Use this only for admin resets, recovery, or testing.
Example
def handle({:command, :admin_reset, _}, context) do
context
|> set_state(:registration, :get_name)
|> answer("Admin reset. Starting registration flow.")
end
@spec start_flow(ExGram.Cnt.t(), atom()) :: ExGram.Cnt.t()
Starts a named flow for the current user/chat.
Sets the flow name, applies the flow's default_state/0 as the initial state
(if defined), and clears any previous data.
Behavior:
- If no flow is currently active → starts the new flow
- If the same flow is already active → re-starts it (resets state + data)
- If a different flow is active → applies the
on_invalid_transitionpolicy. The "from" is the current flow name (treated as a pseudo-state), "to" is the requested flow name.
Example
def handle({:command, :register, _}, context) do
context
|> start_flow(:registration)
|> answer("What's your name?")
end
@spec transition(ExGram.Cnt.t(), atom()) :: ExGram.Cnt.t()
Sets the FSM state with transition validation against the active flow.
This is the normal path for moving through steps. Use set_state/2 only as
an escape hatch when you want to force a state skip validation.
Behavior:
- Reads the current state from
context.extra.fsm.state - Reads the active flow from
context.extra.fsm.flow - Looks up the flow module and checks whether
from -> tois a valid transition - If valid (or no flows configured): updates state and persists to storage
- If NOT valid: delegates to the
on_invalid_transitionpolicy
Return value depends on policy:
:raise— raisesExGram.FSM.TransitionError(never returns normally):log— logs a warning, returnscontextunchanged:ignore— returnscontextunchanged silently{Module, :function}— callsModule.function(context, from, to), returns its result
Example
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! What's your email?")
end
@spec update_data(ExGram.Cnt.t(), map()) :: ExGram.Cnt.t()
Merges a map into the current FSM data, persisting the result.
Uses Map.merge/2 semantics: new keys are added, existing keys are overwritten.
The active flow and state are preserved.
Example
def handle({:text, name, _}, context) do
context
|> update_data(%{name: name})
|> transition(:get_email)
|> answer("What's your email?")
end