pg_fsm is a PostgreSQL extension that embeds finite state machines (FSMs) in your tables.
It stores transitions in schema metadata, enforces event/state consistency through triggers,
and lets you attach callbacks to transitions.
- How It Works
- Requirements
- Install
- Quick Start
- Core Concepts
- Public API Reference
- Validation Rules and Constraints
- Troubleshooting
- Development
- Contributing
- License
For each business table you want to control with a state machine:
fsm.add_to_table(<table>)adds FSM columns and triggers.fsm.add_transition(...)defines allowed transitions infsm.machines.- You append events by updating
new_event. - A trigger computes the new state via
fsm.run_machine(...). - Optional transition callbacks execute after successful updates.
FSM logic is enforced in the database, so invalid transitions fail regardless of the application path used to update rows.
- PostgreSQL 9.6+
- PostgreSQL server development tooling (
pg_configavailable inPATH) - Build toolchain to compile/install PostgreSQL extensions (
make)
git clone https://github.com/brunoenten/pg_fsm.git
cd pg_fsm
make
sudo make installCREATE EXTENSION fsm;The extension is installed in schema fsm and is not relocatable.
This example wires an FSM to an orders table.
-- Example business table
CREATE TABLE public.orders (
id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
customer_id bigint NOT NULL,
total numeric(12,2) NOT NULL
);
-- Attach FSM columns/triggers
SELECT fsm.add_to_table('public.orders'::regclass);
-- Define transitions from start -> pending -> paid|cancelled
SELECT fsm.add_transition('public.orders'::regclass, 'start', 'create', 'pending');
SELECT fsm.add_transition('public.orders'::regclass, 'pending', 'pay', 'paid');
SELECT fsm.add_transition('public.orders'::regclass, 'pending', 'cancel', 'cancelled');
-- Insert row (FSM columns must remain default on INSERT)
INSERT INTO public.orders (customer_id, total) VALUES (10, 99.99);
-- Append one event to move start -> pending
UPDATE public.orders SET new_event = 'create' WHERE id = 1;
-- Append another event to move pending -> paid
UPDATE public.orders SET new_event = 'pay' WHERE id = 1;
SELECT id, fsm_previous_state, fsm_current_state
FROM public.orders
WHERE id = 1;Expected states:
- after insert:
fsm_current_state = 'start' - after
create:fsm_current_state = 'pending' - after
pay:fsm_current_state = 'paid'
Composite type used in event arrays:
name textoccured_at timestamp without time zone
Stores transitions per target table:
table regclassstate_from textevent textstate_to textcallbacks regproc[]
Unique constraint: (table, state_from, event, state_to).
fsm.add_to_table(...) adds:
fsm_events fsm.event[] NOT NULL DEFAULT ARRAY[]::fsm.event[]fsm_current_state text NOT NULL DEFAULT 'start'fsm_previous_state text NOT NULL DEFAULT 'start'new_event text(the only accepted event input; trigger appends it tofsm_eventsand resets it toNULL)
- before trigger (
..._fsm_events_validation) callsfsm.events_trigger()- validates update shape
- computes next state
- after trigger (
..._fsm_events_callbacks) callsfsm.events_callbacks()- executes transition callbacks
After each update that appends exactly one event and recomputes state, the BEFORE trigger calls pg_notify on channel fsm_<table_name>, where <table_name> is the unqualified relation name (TG_TABLE_NAME, for example orders for public.orders).
The payload is a JSON object (text) with:
pk: primary key column names mapped to values (empty object{}if the table has no primary key).old_state: FSM state before this event (OLD.fsm_current_state).event: name of the appended event.new_state: FSM state after running the machine (NEW.fsm_current_state).
Example:
LISTEN fsm_orders;
-- in another session, after a successful transition on public.orders:
-- NOTIFY payload similar to: {"pk":{"id":1},"old_state":"start","event":"create","new_state":"pending"}fsm.add_to_table(_table regclass) RETURNS void- Adds FSM columns and triggers to a table.
fsm.remove_from_table(_table regclass) RETURNS void- Removes FSM columns/triggers and deletes that table's transitions.
fsm.add_transition(_table regclass, _state_from text, _event text, _state_to text) RETURNS voidfsm.remove_transition(_table regclass, _state_from text, _event text, _state_to text) RETURNS voidfsm.execute_transition(_table regclass, initial_state text, _event text) RETURNS text- Looks up the next state for one transition.
fsm.add_callback(_table regclass, _state_from text, _event text, _state_to text, callback_function regproc) RETURNS booleanfsm.remove_callback(_table regclass, _state_from text, _event text, _state_to text, callback_function regproc) RETURNS boolean
Callbacks are executed as SELECT callback(NEW) in the table AFTER UPDATE trigger.
Callback functions should therefore accept a single argument of the target row type.
UPDATE your_table SET new_event = 'event_name' WHERE ...- This is the only supported way to append events.
fsm.append_event(...)- Deprecated and raises an error.
fsm.run_machine(_table regclass, _events fsm.event[]) RETURNS text- Replays all events from
start, returns resulting state. - Raises on invalid transitions.
- Replays all events from
fsm.row_primary_key_jsonb(_table regclass, _row record) RETURNS jsonb- Used by the FSM trigger to build the
pkobject inNOTIFYpayloads.
- Used by the FSM trigger to build the
The extension enforces the following:
- On
INSERT:fsm_eventsmust be empty.fsm_current_stateandfsm_previous_statemust be'start'.new_eventmust beNULL.
- On
UPDATE:- Direct updates to
fsm_eventsare rejected. - At most one event can be appended per update.
new_eventand directfsm_eventsedit cannot be set in the same update.fsm_current_state/fsm_previous_stateare read-only.
- Direct updates to
- Transition metadata:
- Duplicate transitions are rejected.
- Callback list for a transition must not contain duplicates.
- A transition delete may be rejected if it would orphan another transition's origin state.
You attempted to insert custom FSM values. Insert business columns only, then append events via update.
Split batch event appends into multiple updates.
No transition exists for (current_state, event) in fsm.machines for the table.
Add or correct the transition definition.
Ensure callback signatures match the target row type (single argument: NEW row).
This repository includes source SQL under src/ and a generated extension SQL file.
- extension script used by PostgreSQL:
fsm--0.1.sql - source fragments:
src/schemas/fsm/** - build system:
Makefile(PGXS)
makesudo make installThis project uses pg_builder tasks via Rakefile.
bundle install
bundle exec rake build_extensionThis regenerates fsm--<version>.sql from source fragments.
Contributions are welcome.
- Fork and create a feature branch.
- Add tests or reproducible SQL examples for behavior changes.
- Keep SQL comments and README docs in sync with API changes.
- Open a pull request describing motivation and compatibility impact.
GPL-3.0. See LICENSE.txt.