workflow gem

What is workflow?

Workflow is a finite-state-machine-inspired API for modeling and interacting with what we tend to refer to as ‘workflow’.

A lot of business modeling tends to involve workflow-like concepts, and the aim of this library is to make the expression of these concepts as clear as possible, using similar terminology as found in state machine theory.

So, a workflow has a state. It can only be in one state at a time. When a workflow changes state, we call that a transition. Transitions occur on an event, so events cause transitions to occur. Additionally, when an event fires, other arbitrary code can be executed, we call those actions. So any given state has a bunch of events, any event in a state causes a transition to another state and potentially causes code to be executed (an action). We can hook into states when they are entered, and exited from, and we can cause transitions to fail (guards), and we can hook in to every transition that occurs ever for whatever reason we can come up with.

Now, all that’s a mouthful, but we’ll demonstrate the API bit by bit with a real-ish world example.

Let’s say we’re modeling article submission from journalists. An article is written, then submitted. When it’s submitted, it’s awaiting review. Someone reviews the article, and then either accepts or rejects it. Here is the expression of this workflow using the API:

Examples

After installation or downloading of the library you can easily try out all the example code from this README in irb.

$ irbrequire'rubygems'require'workflow'

Now just copy and paste the source code from the beginning of this README file snippet by snippet and observe the output.

Transition event handler

The best way is to use convention over configuration and to define a method with the same name as the event. Then it is automatically invoked when event is raised. For the Article workflow defined earlier it would be:

classArticledefrejectputs'sending email to the author explaining the reason...'endend

article.review!; article.reject! will cause a state transition, persist the new state (if integrated with ActiveRecord) and invoke this user defined reject method.

You can also define event handler accepting/requiring additional arguments:

classArticledefreview(reviewer='')puts"[#{reviewer}] is now reviewing the article"endendarticle2=Article.newarticle2.submit!article2.review!('Homer Simpson')# => [Homer Simpson] is now reviewing the article

The old, deprecated way

We’ve noticed, that mixing the list of events and states with the blocks invoked for particular transitions leads to a bumpy and poorly readable code due to a deep nesting. We tried (and dismissed) lambdas for this. Eventually we decided to invoke an optional user defined callback method with the same name as the event (convention over configuration) as explained before.

Integration with ActiveRecord

Workflow library can handle the state persistence fully automatically. You only need to define a string field on the table called workflow_state and include the workflow mixin in your model class as usual:

classOrder<ActiveRecord::BaseincludeWorkflowworkflowdo# list states and transitions hereendend

On a database record loading all the state check methods e.g. article.state, article.awaiting_review? are immediately available. For new records or if the workflow_state field is not set the state defaults to the first state declared in the workflow specification. In our example it is :new, so Article.new.new? returns true and Article.new.approved? returns false.

At the end of a successful state transition like article.approve! the new state is immediately saved in the database.

You can change this behaviour by overriding persist_workflow_state method.

Custom workflow database column

meuble contributed a solution for using custom persistence column easily, e.g. for a legacy database schema:

Single table inheritance

Single table inheritance is also supported. Descendant classes can either inherit the workflow definition from the parent or override with its own definition.

Custom workflow state persistence

If you do not use a relational database and ActiveRecord, you can still integrate the workflow very easily. To implement persistence you just need to override load_workflow_state and persist_workflow_state(new_value) methods. Next section contains an example for using CouchDB, a document oriented database.

The workflow library itself uses this feature to tweak the graphical representation of the workflow. See below.

Advanced transition hooks

on_entry/on_exit

We already had a look at the declaring callbacks for particular workflow events. If you would like to react to all transitions to/from the same state in the same way you can use the on_entry/on_exit hooks. You can either define it with a block inside the workflow definition or through naming convention, e.g. for the state :pending just define the method on_pending_exit(new_state, event, *args) somewhere in your class.

on_transition

If you want to be informed about everything happening everywhere, e.g. for logging then you can use the universal on_transition hook:

Guards

If you want to halt the transition conditionally, you can just raise an exception in your transition event handler. There is a helper called halt!, which raises the Workflow::TransitionHalted exception. You can provide an additional halted_because parameter.

defreject(reason)halt!'We do not reject articles unless the reason is important' \
unlessreason=~/important/iend

The traditional halt (without the exclamation mark) is still supported too. This just prevents the state change without raising an exception.

Hook order

Multiple Workflows

I am frequently asked if it’s possible to represent multiple “workflows” in an ActiveRecord class.

The solution depends on your business logic and how you want to structure your implementation.

Use Single Table Inheritance

One solution can be to do it on the class level and use a class hierarchy. You can use single table inheritance so there is only single orders table in the database. Read more in the chapter “Single Table Inheritance” of the ActiveRecord documentation. Then you define your different classes:

Individual workflows for objects

Another solution would be to connect different workflows to object instances via metaclass, e.g.

# Load an object from the databasebooking=Booking.find(1234)# Now define a workflow - exclusively for this object,# probably depending on some condition or database fieldif# some conditionclass<<bookingincludeWorkflowworkflowdostate:state1state:state2endend# if some other condition, use a different workflow

You can also encapsulate this in a class method or even put in some ActiveRecord callback. Please also have a look at the full working example!

Documenting with diagrams

You can generate a graphical representation of your workflow for documentation purposes. S. Workflow::create_workflow_diagram.

anywhere in your class. You can also use a simpler function signature like def on_pending_exit(*args) if your are not interested in arguments. Please note: def on_pending_exit() with an empty list would not work.

If both a function with a name according to naming convention and the on_entry/on_exit block are given, then only on_entry/on_exit block is used.