Choreo ⇄ Finitomata: Design, Run, Analyze
Mix.install([
{:choreo, github: "code-shoily/choreo", branch: "main"},
{:finitomata, "~> 0.41"},
{:kino_vizjs, "~> 0.9.0"}
])
Introduction
Choreo and Finitomata are complementary Elixir libraries for finite-state machines:
| Choreo.FSM | Finitomata | |
|---|---|---|
| Feature | Design-time analysis & visualization | Runtime FSM execution with OTP |
| FSM Format |
Graph-based (Yog.Multi.Graph) |
Transition list ([%Transition{}]) |
| Use case | Model, visualize, validate, minimize | Spawn, transition, persist, supervise |
This notebook demonstrates how to bridge them — design in Choreo, run in Finitomata, then import back into Choreo for post-hoc analysis.
The Adapter
This module provides bidirectional conversion between Choreo.FSM and Finitomata.
defmodule FinitomataAdapter do
@moduledoc """
Bidirectional adapter between Choreo.FSM and Finitomata.
- `to_finitomata_dsl/2` — Choreo FSM → Mermaid DSL string for `use Finitomata`
- `from_finitomata/1` — Finitomata module or transition list → Choreo FSM
## Supported syntaxes
* `:flowchart` (default) — Mermaid flowchart syntax. Initial/final states are
implicit; use `auto_terminate: true` in Finitomata to handle final states.
* `:state_diagram` — Mermaid/PlantUML state-diagram syntax. Explicitly emits
`[*]` entry and exit transitions, preserving initial/final state semantics.
## Limitations
* Finitomata allows a single event to target multiple states (non-determinism).
Choreo.FSM is DFA-only, so such transitions are split into distinct labels
like `event→target1`, `event→target2`. Replaying the original event on the
imported Choreo FSM will not work without remapping.
* Choreo transition guards are not represented in Finitomata DSL.
"""
alias Choreo.FSM
@synthetic_events ~w(__start__ __end__ finitomata_back)a
# ── Choreo → Finitomata DSL ─────────────────────────────────────────────
@doc """
Converts a Choreo FSM to a Finitomata-compatible Mermaid DSL string.
## Options
* `:syntax` — `:flowchart` (default) or `:state_diagram`
"""
def to_finitomata_dsl(%FSM{} = fsm, opts \\ []) do
case Keyword.get(opts, :syntax, :flowchart) do
:state_diagram -> to_state_diagram_dsl(fsm)
:flowchart -> to_flowchart_dsl(fsm)
end
end
defp to_flowchart_dsl(fsm) do
fsm.graph.edges
|> Enum.map(fn {_eid, {from, to, label}} -> {from, to, label} end)
|> Enum.reject(fn {from, to, _} -> from == :* or to == :* end)
|> Enum.sort()
|> Enum.map_join("\n", fn {from, to, label} ->
"#{from} --> |#{label}| #{to}"
end)
end
defp to_state_diagram_dsl(fsm) do
initial = FSM.initial_state(fsm)
finals = FSM.final_states(fsm) |> MapSet.new()
entry_lines =
if initial do
["[*] --> #{initial} : __start__"]
else
[]
end
exit_lines =
finals
|> Enum.sort()
|> Enum.map(fn final -> "#{final} --> [*] : __end__" end)
transition_lines =
fsm.graph.edges
|> Enum.map(fn {_eid, {from, to, label}} -> {from, to, label} end)
|> Enum.reject(fn {from, to, _} -> from == :* or to == :* end)
|> Enum.sort()
|> Enum.map(fn {from, to, label} ->
"#{from} --> #{to} : #{label}"
end)
(entry_lines ++ transition_lines ++ exit_lines)
|> Enum.join("\n")
end
# ── Finitomata → Choreo ─────────────────────────────────────────────────
@doc "Builds a Choreo FSM from a compiled Finitomata module."
def from_finitomata(finitomata_module) when is_atom(finitomata_module) do
unless function_exported?(finitomata_module, :__config__, 1) do
raise ArgumentError,
"#{inspect(finitomata_module)} does not appear to `use Finitomata`"
end
finitomata_module.__config__(:fsm) |> from_finitomata()
end
def from_finitomata(transitions) when is_list(transitions) do
user_transitions = Enum.reject(transitions, &(&1.event in @synthetic_events))
initial_states =
transitions
|> Enum.filter(&(&1.from == :*))
|> Enum.flat_map(&List.wrap(&1.to))
|> Enum.uniq()
final_states =
transitions
|> Enum.filter(&(&1.to == :* or (is_list(&1.to) and :* in &1.to)))
|> Enum.map(& &1.from)
|> Enum.uniq()
all_states =
transitions
|> Enum.flat_map(&[&1.from | List.wrap(&1.to)])
|> Enum.reject(&(&1 == :*))
|> Enum.uniq()
choreo =
Enum.reduce(all_states, FSM.new(), fn state, acc ->
cond do
state in initial_states and state in final_states ->
acc |> FSM.add_initial_state(state) |> FSM.add_final_state(state)
state in initial_states ->
FSM.add_initial_state(acc, state)
state in final_states ->
FSM.add_final_state(acc, state)
true ->
FSM.add_state(acc, state)
end
end)
Enum.reduce(user_transitions, choreo, fn t, acc ->
if t.from == :* or t.to == :* or (is_list(t.to) and :* in t.to) do
acc
else
targets = List.wrap(t.to)
event_str = to_string(t.event)
if length(targets) > 1 do
Enum.reduce(targets, acc, fn target, inner_acc ->
FSM.add_transition(inner_acc, t.from, target, label: "#{event_str}→#{target}")
end)
else
FSM.add_transition(acc, t.from, hd(targets), label: event_str)
end
end
end)
end
end
Part 1: Design in Choreo → Generate Finitomata
Let’s model an order processing workflow in Choreo, then generate the Finitomata DSL from it.
1.1 — Build the FSM
alias Choreo.FSM
alias Choreo.FSM.Analysis
order_fsm =
FSM.new()
|> FSM.add_initial_state(:cart)
|> FSM.add_state(:checkout)
|> FSM.add_state(:payment_pending)
|> FSM.add_state(:payment_failed)
|> FSM.add_state(:processing)
|> FSM.add_state(:shipped)
|> FSM.add_final_state(:delivered)
|> FSM.add_final_state(:cancelled)
|> FSM.add_final_state(:refunded)
# Happy path
|> FSM.add_transition(:cart, :checkout, label: "begin_checkout")
|> FSM.add_transition(:checkout, :payment_pending, label: "submit_payment")
|> FSM.add_transition(:payment_pending, :processing, label: "payment_confirmed")
|> FSM.add_transition(:processing, :shipped, label: "ship")
|> FSM.add_transition(:shipped, :delivered, label: "deliver")
# Payment retry
|> FSM.add_transition(:payment_pending, :payment_failed, label: "payment_declined")
|> FSM.add_transition(:payment_failed, :payment_pending, label: "retry_payment")
# Cancellation
|> FSM.add_transition(:cart, :cancelled, label: "cancel")
|> FSM.add_transition(:checkout, :cancelled, label: "cancel")
|> FSM.add_transition(:payment_failed, :cancelled, label: "abandon")
# Refund
|> FSM.add_transition(:shipped, :refunded, label: "refund")
:ok
1.2 — Visualize with Mermaid
mermaid =
order_fsm
|> FSM.to_mermaid()
|> Kino.Mermaid.new()
mermaid_state_diagram =
order_fsm
|> FSM.to_mermaid(syntax: :state_diagram)
|> Kino.Mermaid.new()
dot =
order_fsm
|> FSM.to_dot()
|> Kino.VizJS.render(height: "600px")
Kino.Layout.tabs(
Graphviz: dot,
Mermaid: mermaid,
"Mermaid (State Diagram)": mermaid_state_diagram
)
1.3 — Run Analysis Before Deploying
Before we turn this into a Finitomata module, let’s verify structural soundness.
IO.puts("═══ Structural Validation ═══\n")
validation = Analysis.validate(order_fsm)
case validation do
[] -> IO.puts("✅ No structural issues found.")
issues -> Enum.each(issues, fn {level, msg} -> IO.puts(" #{level}: #{msg}") end)
end
IO.puts("\n═══ Reachability ═══\n")
reachable = Analysis.reachable_states(order_fsm)
all_states = FSM.states(order_fsm)
unreachable = all_states -- reachable
IO.puts(" Reachable: #{inspect(Enum.sort(reachable))}")
IO.puts(" Unreachable: #{inspect(unreachable)}")
IO.puts("\n═══ Dead States ═══\n")
dead = Analysis.dead_states(order_fsm)
case dead do
[] -> IO.puts(" ✅ No dead states (all states can reach a final state).")
states -> IO.puts(" ⚠️ Dead: #{inspect(states)}")
end
IO.puts("\n═══ Livelock Detection ═══\n")
livelocks = Analysis.livelock_states(order_fsm)
case livelocks do
[] -> IO.puts(" ✅ No livelock states.")
states -> IO.puts(" ⚠️ Livelock: #{inspect(states)}")
end
IO.puts("\n═══ Determinism ═══\n")
if Analysis.deterministic?(order_fsm) do
IO.puts(" ✅ FSM is deterministic.")
else
IO.puts(" ⚠️ FSM is non-deterministic (same label from same state to different targets).")
end
IO.puts("\n═══ Shortest Accepting Path ═══\n")
case Analysis.shortest_accepting_path(order_fsm) do
{:ok, path} -> IO.puts(" #{Enum.join(path, " → ")}")
:error -> IO.puts(" ❌ No accepting path found.")
end
1.4 — Generate Test Cases
Choreo can automatically generate test paths covering all states or all transitions:
IO.puts("═══ State-Coverage Test Cases ═══\n")
state_cases = Analysis.generate_test_cases(order_fsm, :state)
state_cases
|> Enum.with_index(1)
|> Enum.each(fn {path, i} ->
IO.puts(" Test #{i}: #{Enum.join(path, " → ")}")
end)
IO.puts("\n═══ Transition-Coverage Test Cases ═══\n")
transition_cases = Analysis.generate_test_cases(order_fsm, :transition)
transition_cases
|> Enum.with_index(1)
|> Enum.each(fn {path, i} ->
IO.puts(" Test #{i}: #{Enum.join(path, " → ")}")
end)
1.5 — Generate Finitomata DSL
Now that the FSM is validated, generate the DSL string for Finitomata.
The adapter supports two Mermaid syntaxes:
-
:flowchart(default) — compact, but initial/final states are implicit. Useauto_terminate: truein Finitomata to handle final states. -
:state_diagram— explicitly emits[*]entry and exit transitions, so initial/final state semantics round-trip faithfully.
IO.puts("═══ Flowchart DSL (default) ═══\n")
flowchart_dsl = FinitomataAdapter.to_finitomata_dsl(order_fsm)
IO.puts(flowchart_dsl)
IO.puts("\n\n═══ State Diagram DSL ═══\n")
state_diagram_dsl = FinitomataAdapter.to_finitomata_dsl(order_fsm, syntax: :state_diagram)
IO.puts(state_diagram_dsl)
Note: Finitomata can also represent non-deterministic transitions (one event → multiple target states). Choreo.FSM is DFA-only, so the adapter splits those into synthetic labels like
event→target. The language is preserved topologically, but event replay requires remapping.
Part 2: Run in Finitomata
Let’s take the generated DSL and use it to define a real Finitomata process.
2.1 — Define the Finitomata Module
defmodule OrderProcess do
@moduledoc "Order processing FSM powered by Finitomata."
use Finitomata,
fsm: """
cart --> |begin_checkout| checkout
cart --> |cancel| cancelled
checkout --> |cancel| cancelled
checkout --> |submit_payment| payment_pending
payment_failed --> |abandon| cancelled
payment_failed --> |retry_payment| payment_pending
payment_pending --> |payment_confirmed| processing
payment_pending --> |payment_declined| payment_failed
processing --> |ship| shipped
shipped --> |deliver| delivered
shipped --> |refund| refunded
""",
auto_terminate: true,
impl_for: :all
@impl Finitomata
def on_transition(current_state, event, _event_payload, state_data) do
updated = Map.update(state_data, :history, [{current_state, event}], &[{current_state, event} | &1])
Finitomata.Transition.guess_next_state(__config__(:fsm), current_state, event, updated)
end
end
2.2 — Verify the Compiled Config
IO.puts("═══ Compiled Finitomata Config ═══\n")
IO.puts("Entry event: #{inspect(OrderProcess.__config__(:entry))}")
IO.puts("States: #{inspect(OrderProcess.__config__(:states))}")
IO.puts("Events: #{inspect(OrderProcess.__config__(:events))}")
IO.puts("\n═══ Transition List ═══\n")
OrderProcess.__config__(:fsm)
|> Enum.reject(&(&1.from == :* or &1.to == :*))
|> Enum.each(fn t ->
IO.puts(" #{t.from} --#{t.event}--> #{inspect(t.to)}")
end)
2.3 — Execute a Live FSM
# Ensure the Finitomata supervision tree is started in our Livebook session
unless Process.whereis(Finitomata.Supervisor), do: Finitomata.start_link()
# Start the happy path
Finitomata.start_fsm(OrderProcess, "order-42", %{user_id: 42, items: ["widget"]})
Process.sleep(50)
# Walk through the happy path
[:begin_checkout, :submit_payment, :payment_confirmed, :ship, :deliver]
|> Enum.each(fn event ->
Finitomata.transition("order-42", {event, nil})
Process.sleep(20)
end)
# Give auto_terminate time
Process.sleep(100)
IO.puts("✅ Order 42 completed the happy path.")
# Ensure Finitomata is running
unless Process.whereis(Finitomata.Supervisor), do: Finitomata.start_link()
# Start the retry + cancel path
Finitomata.start_fsm(OrderProcess, "order-99", %{user_id: 99, items: ["gizmo"]})
Process.sleep(50)
[:begin_checkout, :submit_payment, :payment_declined, :retry_payment, :payment_declined, :abandon]
|> Enum.each(fn event ->
Finitomata.transition("order-99", {event, nil})
Process.sleep(20)
end)
Process.sleep(100)
IO.puts("✅ Order 99 completed the retry→cancel path.")
Part 3: Import Finitomata Back into Choreo
Now let’s demonstrate the reverse — taking a compiled Finitomata module and importing it into Choreo for analysis.
3.1 — Import from Module
imported_fsm = FinitomataAdapter.from_finitomata(OrderProcess)
IO.puts("═══ Imported FSM ═══\n")
IO.puts("States: #{inspect(Enum.sort(FSM.states(imported_fsm)))}")
IO.puts("Initial: #{inspect(FSM.initial_state(imported_fsm))}")
IO.puts("Final: #{inspect(Enum.sort(FSM.final_states(imported_fsm)))}")
IO.puts("Transitions: #{length(FSM.transitions(imported_fsm))}")
3.2 — Visualize the Imported FSM
mermaid =
imported_fsm
|> FSM.to_mermaid()
|> Kino.Mermaid.new()
mermaid_state_diagram =
imported_fsm
|> FSM.to_mermaid(syntax: :state_diagram)
|> Kino.Mermaid.new()
dot =
imported_fsm
|> FSM.to_dot()
|> Kino.VizJS.render(height: "600px")
Kino.Layout.tabs(
Graphviz: dot,
Mermaid: mermaid,
"Mermaid (State Diagram)": mermaid_state_diagram
)
3.3 — Round-Trip Verification
Let’s verify that the round-trip preserves the topology:
IO.puts("═══ Round-Trip Check ═══\n")
original_states = FSM.states(order_fsm) |> Enum.sort()
imported_states = FSM.states(imported_fsm) |> Enum.sort()
original_transitions =
FSM.transitions(order_fsm)
|> Enum.map(fn {from, to, label} -> {from, to, label} end)
|> Enum.sort()
imported_transitions =
FSM.transitions(imported_fsm)
|> Enum.map(fn {from, to, label} -> {from, to, label} end)
|> Enum.sort()
IO.puts("States match: #{original_states == imported_states}")
IO.puts("Transitions match: #{original_transitions == imported_transitions}")
IO.puts("Initial match: #{FSM.initial_state(order_fsm) == FSM.initial_state(imported_fsm)}")
final_match =
Enum.sort(FSM.final_states(order_fsm)) == Enum.sort(FSM.final_states(imported_fsm))
IO.puts("Finals match: #{final_match}")
IO.puts("Behaviorally equivalent? #{Analysis.equivalent?(order_fsm, imported_fsm)}")
Part 4: Analyze a Separately-Created Finitomata
Here’s where the real power lies — importing an FSM you didn’t design in Choreo and finding issues.
4.1 — A Finitomata With Problems
defmodule BuggyWorkflow do
@moduledoc "A workflow with hidden structural issues."
use Finitomata,
fsm: """
idle --> |start| validating
validating --> |valid| processing
validating --> |invalid| rejected
processing --> |complete| done
processing --> |hang| stuck
stuck --> |recover| looping
looping --> |fail| stuck
""",
auto_terminate: true,
impl_for: :all
@impl Finitomata
def on_transition(current, event, _payload, state_data) do
Finitomata.Transition.guess_next_state(__config__(:fsm), current, event, state_data)
end
end
4.2 — Import and Analyze
buggy = FinitomataAdapter.from_finitomata(BuggyWorkflow)
IO.puts("═══ Analysis of BuggyWorkflow ═══\n")
IO.puts("── Validation ──")
case Analysis.validate(buggy) do
[] -> IO.puts(" ✅ No issues")
issues -> Enum.each(issues, fn {level, msg} -> IO.puts(" #{level}: #{msg}") end)
end
IO.puts("\n── Dead States ──")
dead = Analysis.dead_states(buggy)
case dead do
[] -> IO.puts(" ✅ None")
_ -> IO.puts(" ⚠️ #{inspect(dead)} — these states can never reach a final state!")
end
IO.puts("\n── Livelock ──")
livelocks = Analysis.livelock_states(buggy)
case livelocks do
[] -> IO.puts(" ✅ None")
_ -> IO.puts(" 🔄 #{inspect(livelocks)} — these states can cycle forever without reaching acceptance!")
end
IO.puts("\n── Determinism ──")
IO.puts(" #{if Analysis.deterministic?(buggy), do: "✅ Deterministic", else: "⚠️ Non-deterministic"}")
IO.puts("\n── Completeness ──")
IO.puts(" #{if Analysis.complete?(buggy), do: "✅ Complete", else: "⚠️ Incomplete — some states don't handle all events"}")
IO.puts("\n── Shortest Accepting Path ──")
case Analysis.shortest_accepting_path(buggy) do
{:ok, path} -> IO.puts(" #{Enum.join(path, " → ")}")
:error -> IO.puts(" ❌ None")
end
4.3 — Visualize the Buggy FSM
mermaid =
buggy
|> FSM.to_mermaid()
|> Kino.Mermaid.new()
mermaid_state_diagram =
buggy
|> FSM.to_mermaid(syntax: :state_diagram)
|> Kino.Mermaid.new()
dot =
buggy
|> FSM.to_dot()
|> Kino.VizJS.render(height: "600px")
Kino.Layout.tabs(
Graphviz: dot,
Mermaid: mermaid,
"Mermaid (State Diagram)": mermaid_state_diagram
)
4.4 — Invariant Checking
Check if specific properties hold:
IO.puts("═══ Invariant Checks ═══\n")
# Can every reachable state reach at least one final state?
productive? = Enum.empty?(Analysis.dead_states(buggy))
IO.puts("All reachable states productive: #{productive?}")
# Are there any unreachable states?
unreachable = FSM.states(buggy) -- Analysis.reachable_states(buggy)
IO.puts("Unreachable states: #{inspect(unreachable)}")
# Is a forbidden state sequence present anywhere in the graph?
# (This is a static pattern check, not a trace validation.)
forbidden_present = Analysis.violates_invariant?(buggy, forbid_path: [:stuck, :looping, :stuck])
IO.puts("Forbidden loop present? #{forbidden_present}")
Part 5: FSM Minimization
Choreo can minimize an FSM by merging equivalent states, producing a smaller machine that accepts the same language.
5.1 — A Redundant FSM
redundant =
FSM.new()
|> FSM.add_initial_state(:s0)
|> FSM.add_state(:s1)
|> FSM.add_state(:s2)
|> FSM.add_state(:s3)
|> FSM.add_state(:s4)
|> FSM.add_final_state(:s5)
|> FSM.add_final_state(:s6)
# s1 and s2 are equivalent (same outgoing transitions to equivalent targets)
|> FSM.add_transition(:s0, :s1, label: "a")
|> FSM.add_transition(:s0, :s2, label: "b")
|> FSM.add_transition(:s1, :s3, label: "x")
|> FSM.add_transition(:s2, :s4, label: "x")
# s3,s4 → equivalent final states
|> FSM.add_transition(:s3, :s5, label: "done")
|> FSM.add_transition(:s4, :s6, label: "done")
IO.puts("Before: #{length(FSM.states(redundant))} states, #{length(FSM.transitions(redundant))} transitions")
redundant
|> FSM.to_dot()
|> Kino.VizJS.render(height: "600px")
5.2 — Minimize
minimized = Analysis.minimize(redundant)
IO.puts("After: #{length(FSM.states(minimized))} states, #{length(FSM.transitions(minimized))} transitions")
IO.puts("Equivalent? #{Analysis.equivalent?(redundant, minimized)}")
minimized
|> FSM.to_dot()
|> Kino.VizJS.render(height: "600px")
5.3 — Minimize the Order FSM
minimized_order = Analysis.minimize(order_fsm)
IO.puts("Order FSM: #{length(FSM.states(order_fsm))} states → #{length(FSM.states(minimized_order))} states")
IO.puts("Equivalent? #{Analysis.equivalent?(order_fsm, minimized_order)}")
Part 6: Side-by-Side Comparison Table
fsms = %{
"Order (Choreo-designed)" => order_fsm,
"Order (Finitomata-imported)" => imported_fsm,
"BuggyWorkflow (imported)" => buggy,
"Redundant (before minimize)" => redundant,
"Redundant (after minimize)" => minimized
}
header = "| FSM | States | Transitions | Deterministic? | Dead States | Livelocks | Shortest Path |"
separator = "|-----|--------|-------------|----------------|-------------|-----------|---------------|"
rows =
Enum.map(fsms, fn {name, fsm} ->
states = length(FSM.states(fsm))
transitions = length(FSM.transitions(fsm))
det = if Analysis.deterministic?(fsm), do: "✅", else: "❌"
dead = length(Analysis.dead_states(fsm))
livelocks = length(Analysis.livelock_states(fsm))
shortest =
case Analysis.shortest_accepting_path(fsm) do
{:ok, path} -> "#{length(path)} steps"
:error -> "none"
end
"| #{name} | #{states} | #{transitions} | #{det} | #{dead} | #{livelocks} | #{shortest} |"
end)
[header, separator | rows]
|> Enum.join("\n")
|> Kino.Markdown.new()
Part 7: Dynamic Runtime Monitoring with Choreo Heatmaps
Choreo can not only validate and design FSMs, but it can also serve as a live monitoring and visualization tool for running Finitomata instances.
In this section, we will:
-
Spawn 50 simultaneous
OrderProcessinstances representing active user shopping sessions. - Advance them randomly to different stages of the checkout lifecycle.
-
Query the live state of all running Finitomata FSMs using
Finitomata.all/1. - Aggregate state frequencies and render a live traffic heatmap of our production system using Choreo’s heatmap generator.
7.1 — Spawn and Simulate Live Workload
# Ensure Finitomata is running
unless Process.whereis(Finitomata.Supervisor), do: Finitomata.start_link()
# Spawn 50 random orders
order_ids = Enum.map(1..50, fn i -> "order-sim-#{i}" end)
Enum.each(order_ids, fn id ->
Finitomata.start_fsm(OrderProcess, id, %{user_id: :rand.uniform(10_000)})
end)
# Let the FSMs initialize
Process.sleep(50)
# Simulate different user behaviors by transitioning FSMs randomly:
# - Some users stay in cart
# - Some go to checkout
# - Some complete, fail, retry, or cancel
Enum.each(order_ids, fn id ->
# Roll a die to decide how far the order goes
die = :rand.uniform(6)
cond do
die == 1 ->
# Abandon immediately
Finitomata.transition(id, {:cancel, nil})
die == 2 ->
# Stay in checkout
Finitomata.transition(id, {:begin_checkout, nil})
die == 3 ->
# Fail payment and abandon
Finitomata.transition(id, {:begin_checkout, nil})
Process.sleep(10)
Finitomata.transition(id, {:submit_payment, nil})
Process.sleep(10)
Finitomata.transition(id, {:payment_declined, nil})
Process.sleep(10)
Finitomata.transition(id, {:abandon, nil})
die == 4 ->
# Fail payment, retry, and stay pending
Finitomata.transition(id, {:begin_checkout, nil})
Process.sleep(10)
Finitomata.transition(id, {:submit_payment, nil})
Process.sleep(10)
Finitomata.transition(id, {:payment_declined, nil})
Process.sleep(10)
Finitomata.transition(id, {:retry_payment, nil})
die >= 5 ->
# Happy path (processing, shipped, or delivered)
Finitomata.transition(id, {:begin_checkout, nil})
Process.sleep(10)
Finitomata.transition(id, {:submit_payment, nil})
Process.sleep(10)
Finitomata.transition(id, {:payment_confirmed, nil})
if die == 6 do
Process.sleep(10)
Finitomata.transition(id, {:ship, nil})
if :rand.uniform(2) == 1 do
Process.sleep(10)
Finitomata.transition(id, {:deliver, nil})
end
end
end
end)
# Wait a moment for transitions to settle
Process.sleep(100)
IO.puts("Simulation finished. Live instances are resting in various states.")
7.2 — Gather Active States and Render Heatmap
Now we query the registry of all active Finitomata processes, read their current state, and pass those counts to Choreo.Analysis.heatmap/2.
# Retrieve all active FSM names
active_fsms = Finitomata.all() |> Map.keys()
# Count the frequency of each state.
state_counts =
active_fsms
|> Enum.reduce(%{}, fn name, acc ->
case Finitomata.state(name) do
%Finitomata.State{current: state} ->
Map.update(acc, state, 1, &(&1 + 1))
nil ->
acc
end
end)
# Account for non-active FSMs. With auto_terminate: true, most of these
# terminated cleanly, but crashes or failed starts would also appear here.
total_active = Enum.sum(Map.values(state_counts))
non_active_count = 50 - total_active
IO.puts("Active FSMs: #{total_active}, Non-active (mostly auto-terminated): #{non_active_count}")
IO.inspect(state_counts, label: "Active State Frequencies")
# Render the heatmap
order_fsm
|> Choreo.Analysis.heatmap(scores: state_counts, palette: :heat)
|> Choreo.FSM.to_dot()
|> Kino.VizJS.render(height: "600px")
Summary
This notebook demonstrated the full lifecycle:
- Design an FSM in Choreo with rich visualization
- Validate it with reachability, deadlock, livelock, and determinism checks
- Generate test cases automatically for state and transition coverage
-
Export to Finitomata DSL in
:flowchartor:state_diagramsyntax and run it as a supervised OTP process - Import an existing Finitomata module back into Choreo
- Analyze imported FSMs for structural issues you couldn’t see at runtime
- Minimize redundant FSMs while preserving behavioral equivalence
- Monitor live running Finitomata workloads in real-time using Choreo heatmaps