looplab/fsm: a finite state machine library for Go
Finite State Machine for Go
At a glance
- What is it?
- looplab/fsm models states and transitions as plain Go structs, with callbacks and built-in Graphviz and Mermaid output. It suits services that need explicit transition rules and can accept the library's concurrency model.
- Who is it for?
- Use looplab/fsm when a Go service needs an explicit, testable set of states and transitions and you are willing to guard concurrent access yourself. Do not reach for it when the state graph is generated at runtime from user input, or when you need persistence and recovery as part of the model; the library holds state in memory and the README does not document any storage layer.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 35 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What looplab/fsm solves and who reaches for it
Most Go code that tracks a lifecycle starts as a string field and a handful of if statements. A connection is idle, then connecting, then ready; an order is pending, then paid, then shipped. That works until a second developer adds a transition in a different file, or a retry path sets the field directly and skips the validation. The bug is not in any single branch; it is that the legal set of transitions exists only in the heads of the people writing the branches.
looplab/fsm makes that set explicit. You declare states and events, and the library refuses an event that is not allowed from the current state. The README describes it as a finite state machine for Go, and says it is heavily based on two earlier implementations: the JavaScript Finite State Machine by Jake Gordon and Fysom for Python. That lineage explains the shape of the API. Events carry a name, a list of source states and a destination state, which is the same model those two libraries use.
The audience is Go developers writing services, protocol handlers, device or workflow code where the number of states is known at compile time. It is a small library: the top-level repository holds fsm.go, event.go, errors.go, the two visualizers and a handful of example files. There is no runtime, no code generator and no persistence layer in the layout. If you want a state machine that survives a process restart, you are writing that part yourself.
Events, callbacks and the transition path through fsm.go
The core type is fsm.FSM, built by fsm.NewFSM with an initial state, an fsm.Events slice and an fsm.Callbacks map. Each event names the states it may fire from in Src and the state it lands in as Dst. Calling fsm.Event with a context and the event name either performs the transition or returns an error.
Callbacks are keyed by name in that map. The README example for embedding the FSM in a struct registers "enter_state", and the callback receives a *fsm.Event whose Dst field holds the destination state. The examples directory lists additional callback coverage in examples/transition_callbacks.go and a data-carrying example in examples/data.go, so the event object is the place where per-transition payloads travel. The README does not enumerate the full callback set; that detail lives on the godoc page it points to.
Two files in the repository are worth knowing about before you depend on the library. graphviz_visualizer.go and mermaid_visualizer.go, behind a visualizer.go interface, turn the machine into diagram source. That is a real advantage over hand-rolled switch statements: the documentation you draw and the rules you enforce come from the same declaration, so a state added to the Events slice shows up in the diagram. The repository also carries uncancel_context.go, which exists because the event API takes a context and the library needs to distinguish cancellation from transition failure. The release notes for v1.0.3 mention a data race fix and error wrapping, and v1.0.2 mentions a deadlock fix, which tells you the concurrency story has been the active area of maintenance.
Running a first transition with looplab/fsm
The module path is github.com/looplab/fsm and go.mod in the repository declares go 1.16, so any module-aware Go toolchain from that version onward can fetch it. The README points at the godoc page for API docs and examples, and the repository's examples directory holds the runnable programs. The release list shows v1.0.4 as the most recent tag.
The README's examples/simple.go is the shortest complete program. It builds a two-state door, prints the current state, fires open, and fires close:
fsm := fsm.NewFSM(
"closed",
fsm.Events{
{Name: "open", Src: []string{"closed"}, Dst: "open"},
{Name: "close", Src: []string{"open"}, Dst: "closed"},
},
fsm.Callbacks{},
)
err := fsm.Event(context.Background(), "open")With the empty callbacks map, the program prints closed, then open, then closed as the two events succeed. Note the context argument even though nothing in this example cancels it.
The README's examples/struct.go shows the same door embedded in a struct, which is the pattern most services end up using. The FSM becomes a field on the type that owns the state, and the callbacks map registers "enter_state" so the struct can react to each arrival:
type Door struct {
To string
FSM *fsm.FSM
}To see the machine instead of running it, the visualizer files exist for exactly that purpose. The repository layout puts Graphviz and Mermaid output behind one interface, so you can emit diagram source in a test and diff it, or render it in a README. The README does not show the call signature for either visualizer; read graphviz_visualizer.go and mermaid_visualizer.go, or the godoc page the README links, before wiring them into a build step. The Makefile runs the test suite with the race detector enabled:
make testThat target is go test -race ./..., which is the right way to check your own callback code, since the library's own history includes a race fix.
Where looplab/fsm is the wrong tool
The library keeps the current state in memory. Nothing in the repository layout suggests a store, a journal or a recovery path, and the README does not document one. If your process restarts mid-workflow and must resume where it stopped, the state machine alone will not get you there; you need to persist the current state and the data attached to it, and you need to decide what happens when the persisted state is not one your Events slice accepts. That is application work, and it is the part most teams underestimate.
Concurrency is the second boundary. The release history is a sequence of fixes in this area: v1.0.2 fixed a deadlock, v1.0.3 fixed a data race and added error wrapping. That is a normal maintenance record for a library this size, but it also means you should not assume that firing events from several goroutines against one FSM is safe by default. The repository ships uncancel_context.go and examples/cancel_async_transition.go, so cancellation and asynchronous transitions are contemplated, but the README does not state a locking contract. Treat the FSM as owned by one goroutine, or wrap it in your own mutex, and verify with go test -race rather than trusting a guess.
The third case is a state graph that is not known until runtime. If states and transitions come from a config file or a database row that changes between deployments, a struct literal in Go source is the wrong container. You would be rebuilding the Events slice on every load and losing the compile-time checking that is the main reason to pick this library over a switch statement. Similarly, if you only ever have two states and one transition, the dependency buys you little; the diagram output is the one feature that might still justify it.
How it compares with qmuntal/stateless and Django-fsm
qmuntal/stateless appears in the related searches, and the difference in approach is worth stating plainly. It is a Go library that models a state machine as a fluent configuration: you describe states and the triggers that move between them, and you can build the machine from a declarative description rather than a slice of event structs. Where looplab/fsm hands you an fsm.FSM value you call Event on, stateless centers on trigger objects you configure and then fire, which some teams find reads better when the machine is large. Both are Go libraries solving the same problem; the choice comes down to which configuration style fits the codebase and whether you need the visualizer output that looplab/fsm ships in graphviz_visualizer.go and mermaid_visualizer.go.
Django-fsm is a different category entirely. It is a Python package for Django models that integrates state transitions with the ORM, including fields and migrations, so transitions and persistence are one concern. That is the opposite of looplab/fsm's position: this library deliberately knows nothing about storage, which is why it stays small and why resuming after a restart is your problem. If you are in Go and your state lives in a database, you are choosing between a library that models transitions and a framework that also owns the field. The related searches also surface C# FSM and generic "Golang state machine pattern" queries, which usually point at hand-written switch-based implementations. Those have no dependency and no diagram output; looplab/fsm trades a module dependency for a declared transition table and a rendered graph.
Maintenance, licensing and the cost of upgrading
The repository is not archived, and the last push was on 2026-08-27, the same day v1.0.4 was tagged. Before that, v1.0.3 landed on 2025-05-07 and v1.0.2 on 2024-05-16. The pattern is a release roughly once a year, with the most recent one close to the current date. That is a stable, low-churn library, not a fast-moving one, and the version numbers agree: the project is still on 1.x after several years.
The upgrade cost is correspondingly low. Between v1.0.2 and v1.0.3 the changes were a data race fix and error wrapping; between v1.0.3 and v1.0.4 there is no summary in the release list beyond the tag itself. Error wrapping is the kind of change that can alter what errors.Is and errors.As see in your code, so read the diff if you match on specific error values. Nothing else in the release history suggests a breaking API change, and the go.mod floor of go 1.16 means the library will build on toolchains far older than current ones.
Licensing is Apache-2.0, stated at the end of the README and present as LICENSE in the repository root. Apache-2.0 is a permissive licence with an explicit patent grant and a requirement to preserve notices when you redistribute. That matters more for a library you vendor or ship inside a product than for one used only in an internal service. This is a description of the licence terms, not legal advice; if you redistribute the code, have your own counsel read the NOTICE and attribution requirements.
Editorial conclusion
Use looplab/fsm when a Go service needs an explicit, testable set of states and transitions and you are willing to guard concurrent access yourself. Do not reach for it when the state graph is generated at runtime from user input, or when you need persistence and recovery as part of the model; the library holds state in memory and the README does not document any storage layer. Before adopting it, check the release notes for the data race fix in v1.0.3 and the deadlock fix in v1.0.2, run the examples under go test -race, and confirm the callback ordering your design depends on against the godoc page rather than assuming it.
Frequently asked questions
What does FSM stand for in looplab/fsm?
FSM stands for finite state machine. The README describes looplab/fsm as a finite state machine for Go, where a machine has named states and named events that move between them.
What are the types of FSM that looplab/fsm supports?
The README does not classify the library into FSM variants. What it does show is the shape of the model: an initial state, an Events slice where each event lists source states in Src and one destination in Dst, and a Callbacks map keyed by callback name.
How do you design an FSM with looplab/fsm?
Start from the states your system can be in, then write one fsm.Events entry per legal transition, naming the sources in Src and the destination in Dst. Callbacks go in the fsm.Callbacks map, and the repository's graphviz_visualizer.go and mermaid_visualizer.go let you render the result as a diagram to check the design.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/looplab-fsm)