FSMField, FSMIntegerField and FSMKeyField store state in a Django
model column and guard transition methods on that model. They are part of
django-fsm; a separate flow class is not required.
from django.db import models
from django_fsm import FSMField, transition
class Job(models.Model):
status = FSMField(default='new')
@transition(field=status, source='new', target='running')
def start(self):
pass
You can also call status.transition(...) directly on the field:
class Job(models.Model):
status = FSMField(default='new')
@status.transition(source='new', target='running')
def start(self):
pass
FSMField is a CharField with max_length=50 by default.
FSMIntegerField provides the same API with an integer column.
The standalone transition(field=...) decorator accepts either a field
object or its name (field='status'). Names are resolved on the model,
so decorators in ordinary Python mixins also work. Use source='*'
(also the default) for any source state, source='+' for any state except
the target, and target=None to leave state unchanged. An explicitly
registered source takes precedence over a wildcard.
With no permission argument, the standalone decorator’s permission
check allows any user. A string permission such as
permission='myapp.publish_job' checks object permission first, then
global permission through Django’s user.has_perm(). Source and condition checks still apply to direct calls. User permissions
require an explicit has_transition_perm() check or a UI integration.
can_proceed(job.start) checks the source state and conditions;
can_proceed(job.start, check_conditions=False) checks only the source.
has_transition_perm(job.start, user) checks the source, conditions and
permission together. Both helpers can be imported from django_fsm.
The field adds three methods to the model (replace status with your
field name):
get_all_status_transitions() lists all declared transitions.get_available_status_transitions() checks the current source and conditions.get_available_user_status_transitions(user) also checks permissions.Each result exposes name, method, source, target, on_error
and custom.
Methods belonging to another FSM field are excluded.
The direct status.transition(...) form follows the native State API:
use State.ANY for a wildcard, omit target to leave state unchanged,
and set permission explicitly to allow access through permission checks.
The value lives in the model’s own column – no setter/getter pair to
write, Job.objects.filter(status='running') and serialization work as
they would for any CharField. Deferred fields loaded with only() or
defer() fetch their state from the database when first read, including
when a transition needs the current state. The column type is unchanged;
check any generated AlterField migration before applying it.
RETURN_VALUE and GET_STATE can be imported directly from
django_fsm; they are aliases of State.RETURN_VALUE and
State.GET_STATE. They validate resolved dynamic targets
and raise InvalidTargetState for an invalid result.
An ordinary target is applied before calling the method, so
self.status inside the method contains the target state. If the method
raises without an on_error target, django-fsm restores the in-memory
source state and proxy class. This does not roll back database writes made by the method. Use a
database transaction where needed.
GET_STATE also computes and applies its target before the method runs;
RETURN_VALUE applies the target after the method returns.
Native transition metadata uses State.ANY and DEFAULT sentinels.
The model helpers get_all_*_transitions and get_available_*_transitions
expose the '*' and None values. Private
_django_fsm metadata is not recreated; integrations that use it need
separate changes.
Model-field transitions emit pre_transition and post_transition from
django_fsm.signals. Plain State transitions do not emit these
signals.
pre_transition runs after source and condition checks, before the target
is computed or applied. The instance still has its source state.
post_transition runs after a successful method and target validation.
Both signals receive instance, name, field, source, target,
method_args and method_kwargs. The sender is the instance’s source
class, even if a transition switches its proxy class. For dynamic targets,
the pre signal receives the declared target object, and the post signal
receives the resolved value. A transition without a target reports None.
Use on_error on either decorator form to select a state when the method
or dynamic target resolution raises an exception:
@transition(field=status, source='new', target='done', on_error='failed')
def finish(self):
perform_work()
The adapter applies the error state, emits post_transition with that
target and the original exception, then re-raises it. Without an
error target, it restores the source state without emitting the post signal.
State changes are in memory until the model is saved. Zero is a valid error
target for FSMIntegerField.
Rejected sources or conditions do not emit signals or use on_error.
Interrupts such as KeyboardInterrupt restore the source state without
using on_error. Receiver exceptions propagate: a failing pre receiver
prevents the method from running, and a failing post receiver leaves the
completed target state in memory.
FSMKeyField stores a state key in a Django foreign-key column:
from django_fsm import FSMKeyField, transition
class Job(models.Model):
status = FSMKeyField('myapp.JobState', on_delete=models.PROTECT)
@transition(field=status, source='new', target='done')
def finish(self):
pass
Both job.status and job.status_id expose the raw
key, rather than a related model object. Declare sources, targets, defaults
and error targets using those keys. to_field is supported for references
to a unique non-primary-key column. Queryset joins such as
Job.objects.filter(status__label='Done') and model forms use the normal
Django relationship. There is no forward object accessor to prefetch;
retrieve a related state object with its model’s manager when needed.
protected=True protects assignment through both attribute names.
Transitions can still change the key. Deferred state reads load the key
from the database.
Use state_choices instead of choices to map each state to a proxy
model. Each entry contains the value, display label and proxy model name:
class Job(models.Model):
status = FSMField(
default='new',
state_choices=[
('new', 'New', 'NewJob'),
('done', 'Done', 'myapp.DoneJob'),
],
)
class NewJob(Job):
class Meta:
proxy = True
class DoneJob(Job):
class Meta:
proxy = True
Names without an app label resolve within the field’s app. Each referenced model must be a proxy of the same concrete model. Assignment, database loading and transitions select the matching class. An ordinary transition method sees its target proxy because the target is applied before the body runs. For a deferred state field, switching occurs when the state is first loaded. Proxy mappings are preserved by field cloning and migrations.
Add ConcurrentTransitionMixin to a model to reject saves when an observed
FSM state has changed in the database:
from django.db import transaction
from django_fsm import ConcurrentTransitionMixin, FSMField
class Job(ConcurrentTransitionMixin, models.Model):
status = FSMField(default='new')
@status.transition(source='new', target='running')
def start(self):
pass
with transaction.atomic():
job = Job.objects.get(pk=job_id)
job.start()
job.save()
The mixin includes the observed state values in the SQL UPDATE predicate.
If the row still exists but its state no longer matches, save() raises
ConcurrentTransition instead of overwriting it. No version column is
needed. Char, integer and foreign-key state fields are supported, including
multiple fields, proxy models and multi-table inheritance.
Successful saves update the baseline only for saved state columns.
save(update_fields=['text']) checks the states but does not accept an
unsaved transition as a new baseline. refresh_from_db(fields=...) only
resets the baseline for the refreshed fields. With FSMModelMixin, the
default refresh still skips protected fields; retrieve a fresh instance to
reload their persisted state after a conflict.
Deferred states are not fetched during construction. Their baseline is recorded when they are first read or before a direct assignment. If a save checks a state that has never been loaded, it reads that column immediately before the update. It cannot detect changes that happened before that first observation.
Use a transaction around the transition and save to roll back other database
changes on conflict, and catch ConcurrentTransition outside that block.
The mixin does not undo external side effects or restore Python attributes
after a rollback. It compares state values, not the full model: edits to other
columns, or a state that changed and then returned to the same value, are not
versioned. QuerySet.update(), bulk writes and raw SQL bypass these checks.
protected and enforce_initial both default to False, allowing
direct writes and unrestricted initial values unless enabled. Both options are preserved when
Django clones the field or reconstructs it from migrations.
protected=True¶Blocks direct assignment (job.status = 'running') after the instance’s
first value is set, the same way a plain State field already blocks
flow.state_field = x unconditionally. Transitions are unaffected –
they write through State.set(), not attribute assignment.
class Job(models.Model):
status = FSMField(default='new', protected=True)
@status.transition(source='new', target='running')
def start(self):
pass
job = Job.objects.create()
job.status = 'running' # AttributeError
job.start() # fine
Known limitation: protected=True also blocks a plain
instance.refresh_from_db(), since that reassigns the field through the
same guarded path and the slot is already populated by then.
Use FSMModelMixin to refresh other fields while leaving protected state
unchanged:
from django_fsm import FSMField, FSMModelMixin
class Job(FSMModelMixin, models.Model):
status = FSMField(default='new', protected=True)
With this mixin, refresh_from_db() skips protected and deferred fields
by default. It refreshes other loaded fields, leaving protected state in
memory unchanged. An explicit fields=... list is passed through to
Django, so explicitly refreshing an already loaded protected field still
raises AttributeError.
enforce_initial=True¶By default, nothing stops Job.objects.create(status='running') from inserting a row that
never went through start(). With it, creating a row (an INSERT, not
an UPDATE) at any value other than the field’s default raises
NonInitialStateOnCreate, a TransitionNotAllowed subclass – before
the row is written.
class Job(models.Model):
status = FSMField(default='new', enforce_initial=True)
@status.transition(source='new', target='running')
def start(self):
pass
Job.objects.create() # fine -- status='new'
Job.objects.create(status='running') # NonInitialStateOnCreate
The check runs from the field’s pre_save, which Django calls for
.save(), .create(), bulk_create(), and rows inserted through
get_or_create()/update_or_create(). Raw SQL and fixture loading skip
pre_save and are not guarded. Queryset updates also bypass the check;
this option only validates inserted rows.