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
(available since ruote 2.2.0)
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
${’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)