Skip to content

Instantly share code, notes, and snippets.

Last active January 27, 2024 08:29
Show Gist options
  • Save Nagyman/9502133 to your computer and use it in GitHub Desktop.
Save Nagyman/9502133 to your computer and use it in GitHub Desktop.
Workflows in Django

Workflows (States) in Django

I'm going to cover a simple, but effective, utility for managing state and transitions (aka workflow). We often need to store the state (status) of a model and it should only be in one state at a time.

Common Software Uses

  • Publishing (Draft->Approved->Published->Expired->Deleted)
  • Payments
  • Account Authorization (New->Active->Suspended->Deleted)
  • Membership (Trial->Paid->Cancelled)
  • Quality Assurance, Games
  • Anything with a series of steps

Definitely avoid ...

Booleans for states

  • is_new
  • is_active
  • is_published
  • is_draft
  • is_deleted
  • is_paid
  • is_member
  • is_*

Mutually exclusive states ... sort of finite, but the number of states increases with each bool:

  • 2 bools = 2^2 = 4 states
  • 3 bools = 2^3 = 8 states
  • etc (2^N)

Brittle and too many states to check.

Finite State Machine

  • finite list of states
  • one state at a time; the current state
  • transition state by triggering event or condition

The behavior of state machines can be observed in many devices in modern society which perform a predetermined sequence of actions depending on a sequence of events with which they are presented.

Simple approach ...

CharField with defined choices

state = CharField(
    choices=[(1, "draft"), (2, "approved"), (3, "published")]

Define methods to change state:

def publish(self):
    self.state = 3

def approve(self):
    self.state = 2

Better, but ...

  • not enforced
    • Can I go from draft to published, skipping approval?
    • What happens if I publish something that's already published?
  • repetitive
  • side-effects mix with transition code

Some Goals

  • Safe, verifiable transitions between states
  • Conditions for the transition
  • Clear side effects from state transitions
  • DRY


  • declarative transitions and conditions (via decorators)
  • specialized field to contain state

P.S. RoR has similar apps too


  • Specialized CharField
  • Set protected=True
    • to prevent direct/accidental manipulation
    • forces use of transition methods
    • raises an AttributeError "Direct state modification is not allowed"


state = FSMField(
    verbose_name='Publication State',

(alternatives FSMIntegerField, FSMKeyField)

Transition decorator

@transition(field=state, source=[State.APPROVED, State.EXPIRED],
def publish(self):
    Publish the object.

What does this get us?

  • defined source and target states (valid transitions)
  • a method to complete the transition and define side-effects
  • a list of conditions (aside from state), that must be met for the transition to occur


Graphing state transitions

./ graph_transitions -o example-graph.png fsm_example.PublishableModel

Something a bit more complex:


  • submit row
  • logging history
  • noting what's required to change state (messages)


If you'd like your state transitions stored in something other than the admin history.


Not much out there. django-fsm has the most activity.


Craig Nagy @nagyman G Adventures - Software Engineering, eComm Mgr.

Copy link

jrief commented Mar 20, 2016

You should rewrite your pre/post-transition methods to:

    def on_pre_transition(sender, instance=None, target=None, **kwargs):
        if not isinstance(instance, sender):

and use instance as you would with self. Keep in mind, that instance can be an object of any type, hence the optional check if not isinstance(instance, sender)

Copy link

kishan3 commented May 12, 2016

in your example

state = FSMField(
verbose_name='Publication State',

Is the State another model? What are the values for State.CHOICES

Copy link

hoshomoh commented Aug 1, 2016

How do you update the state of a model from an API not necessary the admin dashboard. In my case I am using django-rest-framework

Copy link

caruccio commented Aug 3, 2016

@hoshomoh Just call the transition function from your view.

Copy link

caruccio commented Aug 3, 2016

It took me a while to understand that, in order to persist the state, one must call outside/after @transition function.

@transition(field=state, source='new', target='paid')
def charge(self):
    '''do whatever you need to get paid'''

def pay_invoice_view(request):
    invoice = models.Invoice.get_user_invoice(request.user)
    invoice.charge() ## must save here, not inside charge()

Copy link

Can anybody share a complete sample app pushed to git pls?

Copy link

Thank you. been stuck on this for over a week until i discovered this.

Copy link

beneon commented Nov 1, 2018

Thanks for the post. For the "a bit more complex" example, can you share a little bit about how to implement these transitions?

  1. pre_auth >> payment_auth
  2. payment_auth>>post_auth
    Thanks again.

Copy link

in your example

state = FSMField(
verbose_name='Publication State',

Is the State another model? What are the values for State.CHOICES

an example is in the admin example.

Copy link

cssovs commented Mar 14, 2021

do you support lock?

Copy link

sans712 commented Jun 29, 2021

does this support parallel states in workflows?

Copy link

Would love to see some explicit documentation, i.e. what is source? what is target? How do I change the state?

It wasn't clear that I still need to call .save() after calling the helper fn, or that the helper fn doesn't detect a state change if I directly set the value; it only provides a home for side effects + enforces restrictions on state change directions.

Copy link

blueyed commented Jun 1, 2022

Copy link

CARocha commented Mar 14, 2023

You say "P.S. RoR has similar apps too !! " which?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment