workflow control patterns

The Workflow Patterns are a catalog of various building blocks for workflow execution.

Described here are ways to implement each of those patterns with ruote. Some of them are not directly realizable with ruote, approximations are proposed. This is a self-evaluation, for an authoritative voice, the workflow patterns website and its mailing list are here.

Each pattern is illustrated with a Ruby DSL implementation (or approximation). XML implementations are easily derivable from their Ruby counterparts. There is also a link to the original pattern explanation and its flash animation.

Participant expressions have been supplemented with an :activity or :task attribute to give a better feel about an hypothetical context for the application of the pattern.


Basic Control Flow Patterns

sequence

Chaining activities in sequence. Uses the sequence expression. The cursor expression might also be used.

              sequence do
                participant :ref => 'alpha', :activity => 'write'
                participant :ref => 'bravo', :activity => 'fix typos'
              end
            

original pattern explanation | top

parallel split

The concurrence expression is the main tool for ‘parallel splits’.

              concurrence do
                participant :ref => 'alpha', :activity => 'write introduction'
                participant :ref => 'bravo', :activity => 'write postface'
              end
            

original pattern explanation | top

synchronization

Synchronization is supported implicitely by the concurrence expression. Note that this expression can be tuned via its attributes for behaviours different than the vanilla “wait before all child expressions have replied” one.

original pattern explanation | top

exclusive choice

Exclusive ‘routing’ within the process : the flow will go one way or the other, but not both.

The if is usually in charge when implementing this pattern (the underscore ‘_’ prefixing the if prevents collision with the ‘if’ Ruby keyword).

              _if '${f:decision} == accepted' do
                participant :ref => 'alpha', :activity => 'request further info'
                participant :ref => 'alpha', :activity => 'send refusal note'
              end
            

(this if example forwards the flow to the same ‘alpha’ participant, but with different activities).

One could rewrite this as :

              sequence do
                participant 'alpha', :activity => 'request further info', :if => '${f:decision} == accepted'
                participant 'alpha', :activity => 'send refusal note', :if => '${f:decision} == refused'
              end
            

But this is not strictly equivalent

With a bit of imagination, exclusive choices may be found beyond ifs :

              Ruote.process_definition :name => 'request processing' do
            
                sequence do
                  # ... 
                  participant :ref => 'editor' # decision : accepted or refused
                  subprocess :ref => 'request_${f:decision}'
                  # ...
                end
            
                define 'request_accepted' do
                  participant :ref => 'alpha', :activity => 'request further info'
                end
            
                define 'request_refused' do
                  participant :ref => 'alpha', :activity => 'send refusal note'
                end
              end
            

The name of the subprocess is extrapolated at runtime and the flow is routed accordingly.

original pattern explanation | top

simple merge

A simple merge occur when two (or more) exclusive branch converge. As seen in exclusive choice this pattern is implicitely supported. It simply occurs when the ‘then’ or the ‘else’ clause of an ‘if’ terminates and the flow resumes.

original pattern explanation | top


Advanced Branching and Synchronization Patterns

multi choice

Combining the concurrence expression and :if attributes makes for an easy implementation of this pattern :

              concurrence do
                participant 'regular service', :if => '${f:price} < 50'
                participant 'premium service', :if => '${f:price} > 40'
                participant 'extra service', :if => '${f:extra_ordered}'
              end
            

In this example, if the price is between 40 and 50, regular service and premium service are triggered, extra could be triggerd as well if the field ‘extra_ordered’ has the value ‘true’.

original pattern explanation | top

structured synchronizing merge

The multi choice pattern implementation implicitely support this ‘structured synchronizing merge’. The concurrence expression waits for all its children expression to reply before resuming the flow.

original pattern explanation | top

multi merge

The convergence of two or more branches into a single subsequent branch such that each enablement of an incoming branch results in the thread of control being passed to the subsequent branch.

The flash animation for this pattern is worth a look.

Basically, what comes after the multi-merge will be executed once for each incoming replies.

Certainly, ruote doesn’t support that out of the box, but there are approximations to it :

              Ruote.process_definition :name => 'multi merge' do
            
                concurrence do
                  sequence do
                    team_a
                    after
                  end
                  sequence do
                    team_b
                    after
                  end
                  sequence :if => '${f:more_capacity_needed}' do
                    extra_team
                    after
                  end
                end
            
                define 'after' do
                  participant 'supervisor', :activity => 'assess work'
                end
              end
            

Here, a subprocess (define ‘after’) is used after each concurrent branch. Before replying to its parent (the concurrence), each branch calls it. (team_a, team_b and extra_team could be participant or subprocess names).

Leveraging the listen expression is another option :

              Ruote.process_definition :name => 'multi merge' do
            
                concurrence :count => 3, :remaining => :forget do
            
                  team_a
                  team_b
                  team_extra :if => '${f:more_capacity_needed}'
            
                  listen :to => /^team\_.+/, :upon => :reply do
                    participant 'supervisor', :activity => 'assess work'
                  end
                end
              end
            

With this process definition, each time a team participant replies, the participant ‘supervisor’ is sent a workitem. Please note that this works only with participants. In the above subprocess example, ‘team_a’ could point either to a participant, either to a subprocess.

original pattern explanation | top

structured discriminator

As soon as one of two concurrent branches replies, the flow resumes and the other branch is ‘forgotten’ (continues its execution without the parent process caring for its outcome).

              sequence do
                concurrence :count => 1, :remaining => :forget do
                  test_batch_a
                  test_batch_b
                end
                final_test_batch
              end
            

As soon as an initial test batch is completed, the final test batch is triggered. The remaining test batch continues, unhindered (but forgotten).

The attributes :count and :remaining of the concurrence are used. Note that those attributes are understood by the concurrent_iterator expression as well :

              sequence do
                concurrent_iterator :on => 'a, b, c, d', :count => 1, :remaining => :forget do
                  subprocess :ref => 'test_batch_${v:i}'
                    # batches a, b, c and d will get triggered
                end
                final_test_batch
              end
            

original pattern explanation | top


Structural Patterns

arbitrary cycles

The ability to represent cycles in a process model that have more than one entry or exit point.

Thanks to the cursor expression, the process represented as flash animation on the Workflow Patterns site may be implemented as :

              cursor do
                participant 'a'
                participant 'b'
                participant 'c'
                jump :to => 'b', :if => '${f:back_to_b}'
                participant 'd'
                jump :to => 'c', :if => '${f:back_to_c}'
                participant 'e'
              end
            

The jump expression understand participant names, subprocess names and :tag names in its :to attribute, making it easy to specify jump points.

original pattern explanation | top

implicit termination

Replicating the flash animation of the pattern, we obtain :

              sequence do
                participant 'alpha'
                concurrence do
                  sequence do
                    participant 'bravo'
                    participant 'delta'
                  end
                  sequence do
                    participant 'charly'
                    participant 'echo'
                  end
                end
              end
            

There is not much to say about this pattern. When there is no more work, the process ends.

original pattern explanation | top


Multiple Instance Patterns

multiple instances without synchronization

within a given process instance, multiple instances of an activity can be created. These instances are independent of each other and run concurrently. There is no requirement to synchronize them upon completion.

              concurrent_iterator :times => 10 do
                activity :forget => true
              end
            

‘activity’ here could be a participant or a subprocess named ‘activity’. :forget is set to true (no requirement to synchronize). The process will resume immediately after this concurrent_iterator (since all its children are forgotten).

The flash animation could be translated to something like :

              sequence do
                participant 'a'
                concurrent_iterator :times => '${f:count}' do
                  participant 'b', :forget => true
                end
                participant 'c'
              end
            

The number of ‘b’ instances to fire is expected to be found in the workitem field named ‘count’ (value is perhaps chosen by participant ‘a’).

original pattern explanation | top

multiple instances with a priori design time knowledge

the required number of instances is known at design time. These instances are independent of each other and run concurrently. It is necessary to synchronize the activity instances at completion before any subsequent activities can be triggered.

The concurrence or the concurrent_iterator expressions can be used.

              concurrence do
                participant 'alfred'
                participant 'bertha'
                participant 'charly'
              end
            

which can be rewritten as :

              concurrent_iterator :on => 'alfred, bertha, charly', :to_field => 'p' do
                participant :ref => '${f:p}'
              end
            

original pattern explanation | top

multiple instances with a priori run time knowledge

Within a given process instance, multiple instances of an activity can be created. The required number of instances may depend on a number of runtime factors, including state data, resource availability and inter-process communications, but is known before the activity instances must be created. Once initiated, these instances are independent of each other and run concurrently. It is necessary to synchronize the instances at completion before any subsequent activities can be triggered.

It’s again a job for the concurrent_iterator expression.

              concurrent_iterator :on => '${f:reviewer_list}', :to_field => 'p' do
                participant :ref => '${f:p}'
              end
            

Here, the list of participant that will ‘perform’ concurrently is help in a workitem field named ‘reviewer_list’ (a comma-separated list of participant names).

original pattern explanation | top

multiple instances without a priori run time knowledge

The number of activity is known at runtime and may change at any point during the execution of these activities.

The expression add_branches which works in conjunction with the concurrent_iterator expression is necessary for this pattern implementation.

              concurrence :count => 1 do
            
                concurrent_iterator :on => '${f:reviewer_list}', :tag => 'review' do
                  participant :ref => '${v:i}'
                end
            
                repeat do
                  participant 'supervisor', :q => 'add more reviewers ?'
                  add_branches '${f:new_supervisors}', :ref => 'review'
                end
              end
            

The review work is performed in the concurrent_iterator tagged ‘review’. Concurrently a participant supervisor is asked if more reviewers are needed. He has the opportunity to add more reviewers as long as the concurrent_iterator is not over. Since the wrapping concurrence has a :count of 1, as soon as the iterator is done, the concurrence will be over (the supervisor branch getting cancelled).

The add_branches expression knows it has to add the supervisor to the concurrent iterator via the :ref => ‘review’ declaration.

original pattern explanation | top


State-based Patterns

deferred choice

This pattern is difficult for ruote to implement. It requires some control over the state of the workitem as a task, and since ruote delegates task handling to participants…

A set of participants is presented with a task, as soon as one of them starts working on it, the other instances of the task are withdrawn from the other participants.

With a bit of help from the participant implementations, this could be a realization :

              sequence do
            
                # presenting the task
            
                concurrence :count => 1 do
                  participant 'a', :task => 'start work'
                  participant 'b', :task => 'start work'
                end
            
                # performing the task
            
                participant '${a_or_b}', :task => 'perform work'
              end
            

As soon as ‘a’ or ‘b’ replies (concurrence :count => 1), it is presented with the task again. The other participant receives a cancel notification.

Of course, the workitem/task presented is not strictly equivalent to the task to be performed…

Perhaps the better way to implement this pattern is within the participant itself. The ruote engine could dispatch the task to participant ‘x’, a worklist handler managing access rights, escalation and so and so independently.

original pattern explanation | top

interleaved parallel routing

TODO

original pattern explanation | top

milestone

A task is only enabled when the process is in a specific state.

Here is a ruote attempt at implementing this pattern :

            Ruote.define do
            
              concurrence do
            
                sequence do
                  participant 'a'
                  participant 'b', :tag => 'milestone'
                  participant 'c'
                end
            
                concurrence :count => 1 do
                  # :count => 1 means the concurrence terminates as soon as 1 branch
                  # replies. The other branch is cancelled.
            
                  sequence do
                    listen :to => 'milestone', :upon => 'entering', :wfid => true
                      # blocks until milestone is reached
                    participant 'd'
                  end
            
                  listen :to => 'milestone', :upon => 'leaving', :wfid => true
                    # blocks until milestone is left
                end
              end
            end
            

In this implementation, the participant ‘d’ receives a workitem as soon as the milestone is reached (participant ‘b’ receives a workitem). d’s task is over as soon as d terminates it or b terminates his task.

original pattern explanation | top


Cancellation Patterns


New Control Flow Patterns

coming soon.