dollar notation

(for documentation about conditions and the dollar notation, see conditions)

Most of the examples feature plainly named participant like

              sequence do
                participant 'alfred'
                participant 'bob'
              end
            

but the real world is more dynamic:

              sequence do
                participant '${f:patient}'
                participant '${f:doctor}'
              end
            

In that mini process definition, the workitem is routed from the patient to the doctor. The actual participant name is held in the workitem field “patient” and then in the field named “doctor”. Since it’s a sequence, the value in the field doctor could have been set by the patient.

The prefix f comes for field, as in workitem field.

This “dollar notation” scans strings in process definition for ${...} constructs and substitutes them for their workitem field or process variable value.

If ruby_eval_allowed is set to true, ${r:code} will evaluate (after a security check) the code and place the result in the target string.

Not all dollar substitutions result in a string, there is a technique for pointing a field or variable values literally.

variables and workitem fields

(the main piece of documentation about this subject is at process variables and workitem fields)

Two types of data are available to process instances: process variables and workitem fields.

The most visible type is workitem fields. Each workitem has a dictionary of fields as payload. Workitem and their fields are visible to the participants (ie outside of the workflow engine itself).

Process variables are not communicated outside of the engine (except if the set expression is used to copy a variable to a field). Variables are mainly used for routing decisions inside the process instance.

            Ruote.process_definition :name => 'loan_approval', :revision => '1' do
              cursor do
                participant 'planning team'
                concurrence do
                  participant 'ceo', :if => '${f:total} > 100000'
                  participant 'cfo'
                end
                rewind :unless => '${f:approved}'
                participant 'execution team'
              end
            end
            

In this first example process definition, there are two fields visible from the process definition: ‘total’ and ‘approved’. The total has been determined by the planning team, while the ‘approved’ field will be set by the cfo (and perhaps the ceo).

Note that if the total is superior to 100’000, the ceo, concurrently with the cfo, will receive a workitem. Thus the workitem (and its fields) will get duplicated, one copy for each concurrence branch. Process variables never get “cloned” in this way.

            Ruote.process_definition :name => 'loan_approval', :revision => '2' do
              cursor do
                participant 'planning team'
                concurrence do
                  participant 'ceo', :if => '${v:supervised}'
                  participant 'cfo'
                end
                rewind :unless => '${f:approved}'
                participant 'execution team'
              end
            end
            

In that example, if the process variable “supervised” is set to true, the ceo will have his say in the iterations. The information about whether or not the process instance is supervised is not passed to participants (well obviously the ceo will know).

by default ${key} points to the field ‘key’

Since ruote 2.1, the following two process definitions are equivalent:

              sequence do
                participant '${f:patient}'
                participant '${field:doctor}'
              end
            
              sequence do
                participant '${patient}'
                participant '${doctor}'
              end
            

deep lookup (composite keys)

Imagine you have this process definition:

            Ruote.process_definition :name => 'contract preparation' do
              sequence do
            
                set 'field:customer' => {
                  'name' => 'Dexter Shipping',
                  'address' => [ 'Orchard Road', 'Singapore' ]
                }
                  # setting some field f one way or the other
            
                # ...
                participant 'legal dept', :task => 'draft contract for ${f:customer.name}'
                # ...
                participant 'asia rep', :if => "${f:customer.address.1}' == 'Singapore'"
                # ...
              end
            end
            

It’s OK to use dots and key names or integer indexes to look deep inside of a field or a variable (${v:partner.address.city}).

initial fields and initial variables

Looking at the above example, one thing should be clear: our customer information is hardcoded in the business process.

Why not remove it?

            pdef = Ruote.process_definition :name => 'contract preparation' do
              sequence do
                participant 'legal dept', :task => 'draft contract for ${f:customer.name}'
                # ...
                participant 'asia rep', :if => "${f:customer.address.1}' == 'Singapore'"
                # ...
              end
            end
            

and pass the information in due time, i.e. at launch time?

            ruote_engine.launch(
              pdef,
              { 'customer' => {
                'name' => 'Dexter Shipping',
                'address' => [ 'Orchard Road', 'Singapore' ] } })
            

The signature for the launch method looks like

            def launch(process_definition, fields={}, variables={})
            

Fields are the initial fields (payload) of the workitem and variables are the initial variables at the root of the process instance getting launched.

mixing and nesting

Mixing and nesting is OK.

            Ruote.process_definition do
              # ...
              set 'f:a' => 'al'
              set 'f:b' => 'fred'
              set 'f:alfred' => 'albert'
              # ...
              participant '${a}'         # participant 'al'
              participant '${b}'         # participant 'fred'
              participant '${a}${b}'     # participant 'alfred'
              participant '${al${b}}'    # participant 'albert'
              participant '${${a}${b}}'  # participant 'albert'
            end
            

${fei}, ${wfid}, ${subid}, ${expid}, and ${engine_id}

This tiny program:

            require 'rubygems'
            require 'ruote'
            
            engine = Ruote::Engine.new(Ruote::Worker.new(Ruote::HashStorage.new()))
            
            engine.register_participant :wrongdoer do |workitem|
              raise "I did something wrong"
            end
            
            pdef = Ruote.process_definition do
              sequence do
                echo 'fei       : ${fei}'
                echo 'wfid      : ${wfid}'
                echo 'expid     : ${expid}'
                echo 'subid     : ${subid}'
                echo 'engine_id : ${engine_id}'
              end
            end
            
            wfid = engine.launch(pdef)
            
            engine.wait_for(wfid)
            

will emit to the standard output something like:

            fei       : 0_0_0!!20100407-bipopopizu
            wfid      : 20100407-bipopopizu
            expid     : 0_0_2
            subid     : 2cf8837dec6eda1f5da484a2a5bb4bc5
            engine_id : engine
            

These are not the content of the fields fei, wfid, expid, subid. It’s the flow expression id, the workflow instance id, the expression id and the sub workflow instance id.

The fei is a compound of the three others.

This technique can be used in various ways. Here is an example:

            Ruote.process_definition :name => 'new batch' do
              sequence do
                set 'batch_id' => 'batch--${wfid}'
                clerks :task => 'input new batch'
                testers :task => 'schedule tests for new batch'
              end
            end
            

The field batch_id is set to something like batch--20100407-barakuraba.

$x, $f:x, $v:y

All the dollar examples so far result in strings being composed. What if you want the want the literal workitem field or process variable blue to be given ?

            Ruote.process_definition do
              set 'v:customers' = [ 'Steiner', 'Stransky', 'Brandt' ]
              set 'v:order_id' = 12345
              # ...
              participant 'salesman', :customers => '$v:customers', :order => '$v:order_id'
            end
            

Note the lack of curly brackets (and the single reference). This literal technique only works with when the final substitution is of the form “^\$[^{}:]+$”.

No substitution will occur:

            Ruote.process_definition do
              echo ' $x '    # => ' $x '
              echo ' $v:x '  # => ' $v:x '
              echo ' $v:x'   # => ' $v:x'
              # ...
            end
            

Nesting and mixing is OK as well, as long as the final substitution lacks the curly brackets :

            Ruote.process_definition do
              # ...
              set 'v:a' => 'al'
              set 'v:b' => 'fred'
              set 'v:alfred' => { 'name' => 'Alfred', 'age' => 23 }
              # ...
              subprocess 'x', :customer => '$v:${a}${b}'
                # customer will hold the 'alfred' hash
            end
            

dollar notation and missing values

A vanilla expression like “${f:x}” will yield “${f:x}” when there is no workitem field named “x” (missing value). The same holds for “$x”, it’d yield “$x”… Up until ruote 2.3.0 included. From ruote 2.3.1 on, “$x” will yield nil (we expect a value, not a string value as in the “${f:x}” case.

${’f:x} and ${"f:x} quoting

The dollar notation has a special little trick. If you place a ’ or a " right after the opening {, the resulting string will get double-quoted.

            emit_invoice :if => '${"name} == "joe smunch"'
            

if the workitem field named ‘name’ holds “jeff bunch”, the comparison will look like

            '"jeff bunch" == "joe smuch"'
            

(and yield false).

ruby_eval_allowed

When the engine is configured with ‘ruby_eval_allowed’ => true, which boils down to

            engine = Ruote::Engine.new(
              Ruote::Worker.new(
                Ruote::HashStorage.new('ruby_eval_allowed' => true)))
            

a special (and dangerous) r or ruby prefix gets unlocked.

            Ruote.process_definition :name => 'new batch' do
              sequence do
                set 'batch_id' => '${r:Operations.generate_batch_id}'
                clerks :task => 'input new batch'
                testers :task => 'schedule tests for new batch'
                clerks :task => 'schedule transport',
                  :if => '${r:Operations.no_transport_available?}'
              end
            end
            

This process definition calls to methods from the Operations module, one to generate a batch id and the second to check if transports are available.

Ruote tries is best to prevent dangerous operations from happening whithin the ${r:xxx} envelope, but beware! Especially if your process definitions are loaded from untrusted sources.

The Ruby code within ${r:xxx} has access to

  • flow_expression, fe, fexp: the FlowExpression itself
  • fei: the id of the FlowExpression
  • workitem, wi: the workitem
  • engine_id: the id of the engine
  • d(): a way to bring back regular dollar substitution inside of ${r:…}
  • “method_missing”: a shortcut to workitem fields

Here is a potpourri of those:

            require 'rubygems'
            require 'ruote'
            
            engine = Ruote::Engine.new(
              Ruote::Worker.new(
                Ruote::HashStorage.new('ruby_eval_allowed' => true)))
            
            #engine.noisy = true
            
            pdef = Ruote.define do
            
              set 'v:var0' => 'hello'
            
              echo '${r:fei.sid}'
                # => 0_1!51ea612b76a8b2670050abf4583b9406!20110218-besushipoki
              echo '${r:wi.fields.size}'
                # => 1
              echo '${r:fexp.name}'
                # => echo
              echo "${r:d('customer.address.0').downcase}"
                # => sycamore street 1
              echo "${r:d('v:var0')}"
                # => hello
              echo "${r:customer['name']}"
                # => Alain
            end
            
            wfid = engine.launch(
              pdef,
              'customer' => {
                'name' => 'Alain',
                'address' => [ 'Sycamore Street 1', 'Triple Peaks' ]
              })
            
            engine.wait_for(wfid)
            

see also