attributes common to all expressions
The attributes listed on this page may be used with any expression.
- :timeout
- :if / :unless
- :forget
- :lose
- :flank
- :on_error
- :on_cancel
- :on_timeout
- :tag
- :filter
- :take
- :discard
- :timers
- :scope
- :await
:timeout
If after two days, the two reviewers couldn’t do their work, the process instance will resume to the editor :
sequence do
participant :ref => 'author'
sequence :timeout => '2d' do
participant :ref => 'reviewer1'
participant :ref => 'reviewer2'
end
participant :ref => 'editor'
end
:timeout understands h, m, d, s (respectively hour, minute, day, second). It also understands y, M, w (for year, Month and week), but they are rarely used.
It’s OK to give an absolute date to the :timeout attribute :
participant :ref => 'author', :timeout => 'Sun Jan 24 17:28:28 +0900 2010'
But most of the time absolute dates are fetched from process variables or workitem fields :
participant :ref => 'author', :timeout => '${f:time_limit}'
Please note that participants may have their say in their timeout.
You might also have a look at the :on_timeout attribute.
:if / :unless
These two attributes accept a condition string. If the condition evaluates to true (or false for :unless), the expression will get executed, else not.
The CEO will receive a workitem / task only if the budget (stored in a workitem field) exceeds 23000 :
concurrence do
participant 'ceo', :if => '${f:budget} > 23000'
participant 'cfo'
participant '${f:bu_head}'
end
Any expression may use :if / :unless :
cursor do
subprocess 'gather_data'
subprocess 'generate_graphs'
participant 'quality_control'
rewind :unless => '${f:sufficient_data}'
subprocess 'generate_pdfs'
# over
end
Really any :
sequence do
sequence :if => '${f:weather} == rainy' do
rent_tent
rent_heating_system
end
concurrence do
sequence do
emit_invitations
gather_responses :timeout => '3w'
end
cursor :if => '${f:orchestra}' do
gather_orchestra
decide_about_orchestra
reserve_orchestra
rewind :if => '${f:orchestra_already_taken}'
end
participant 'mayor', :task => 'notification'
end
end
The :if and :unless conditions understand things like !=, ==, =~, ‘is set’, ‘is empty’, &&, ||, … More information in the conditions page.
:forget
This is the attribute equivalent of the forget expression.
An expression flagged with :forget => true or :forget => ‘true’ gets forgotten, it is considered has having replied immediately to its parent expression, though its ‘execution’ is resuming independently.
concurrence do
participant 'alfred'
participant 'bob'
participant 'charly', :forget => true
end
Charly will receive a workitem, but the concurrence will receive a reply immediately, thus, the concurrence (and the rest of the process) will resume as soon as both Alfred and Bob have replied.
It can be used for some kind of rough fire and forget concurrency :
sequence do
participant 'alfred', :forget => true
participant 'bob', :forget => true
participant 'charly', :forget => true
end
:lose
This is the attribute equivalent of the lose expression.
Ruote.process_definition do
concurrence :count => 1 do
alfred
sequence :lose => true do
wait '2d'
send_reminder_to_alfred
wait '2h'
send_alarm_to_boss
end
end
end
:timers are probably a better way to express that business logic with ruote.
:flank
(introduced in ruote 2.3.0)
The previous ‘lose’ example can be rewritten with ‘flank’ as:
Ruote.process_definition do
sequence do
sequence :flank => true do
wait '2d'
send_reminder_to_alfred
wait '2h'
send_alarm_to_boss
end
alfred
end
end
‘Flanking’ expressions will reply to their parent expression immediately (like :forget) but will still be cancellable (unlike :forget).
Since they are cancellable, they won’t outlive their parent expressions. Thus
Ruote.process_definition do
sequence do
bob :task => 'support work', :flank => true
alfred :task => 'main effort'
end
end
the support work of bob will terminate (get cancelled) as soon as alfred is done with his main effort.
Granted, this could previously be done with “concurrence :count => 1” and “:lose => true”, but since ‘flanks’ where introduced with timers, the :flank attribute was introduced as well.
To sum up the difference between forget, lose and flank:
| attribute / expression | replies to parent | cancellable |
|---|---|---|
| normal expression | as soon as job is done | yes |
| forget | immediately | no (not reachable) |
| lose | never | yes |
| flank | immediately | yes |
:on_error
By default, any error in a process instance gets logged and the segment of process where it occurred is stalled. It’s then possible to replay_at_error() the issue.
What if you want to specify the “on error” behaviour directly in the process definition ?
:on_error is the closest thing to the begin/rescue, try/catch found in regular programming languages.
Ruote.define :name => 'x' do
sequence :on_error => 'handle_issue' do
participant 'alpha'
cursor do
# ...
end
end
define 'handle_issue' do
participant 'supervisor', :msg => 'process ${wfid} has gone ballistic'
end
end
If there is an error (at any level/depth) inside of our sequence, the whole branch of the “sequence” will get cancelled and then replaced by the element indicated in :on_error.
There will be no error registered in the error journal (unless there is an error in the handling participant/subprocess itself).
:on_error must point to a subprocess or a participant, or a command like “redo” or “undo”.
When it points to a subprocess, the branch in error gets replaced by an instance that subprocess. When it points to a participant branch gets replaced by a single workitem despatchement to the participant.
composing with on_error (on_error: “retry * 3, pass”)
Since ruote 2.3.0, the on_error attribute understands handler composition. One can write things like:
# retry three times, immediately
handover_a :on_error => "retry * 3"
# retry three times, then give up
handover_b :on_error => "retry * 3, pass"
# retry three times, each time after 1 minute, then give up
handover_c :on_error => "1m: retry * 3, pass"
# retry after 1 second, 1 minute, finally one hour
handover_d :on_error => "1s: retry, 1m: retry, 1h: retry"
Now on for the list of messages on_error understands.
on_error: redo
If :redo or ‘redo’ is given, the process branch will get cancelled and retried :
sequence :on_error => :redo do
# ...
end
on_error: undo
If :undo or ‘undo’ is passed, the branch will get cancelled and the flow will resume :
sequence do
participant 'notify_bu', :on_error => :undo
# we don't care if the notification fails
participant 'bu_head'
# business as usual
end
on_error: retry, pass
‘retry’ / :retry and ‘pass’ / :pass are aliases for ‘redo’ and ‘undo’ respectively.
sequence do
participant 'notify_bu', :on_error => :pass
# we don't care if the notification fails
participant 'bu_head', :on_error => :retry
# retry until no error
end
The listen expression may also listen to errors (from ruote 2.3.0 on).
If you want to register finer grained error interception, you might be want to have a look at the on_error expression (from ruote 2.3.0 on).
on_error: cancel
By specifying ‘cancel’, one can tell ruote to trigger the on_cancel handler in case of error.
cursor :on_cancel => 'decommission', :on_error => 'cancel' do
# ...
end
on_error: cando
‘cando’ is “cancel-redo” collapsed. The :on_cancel is triggered (can__) then the faulty expression (tree) is retried (__do).
cursor :on_cancel => 'decommission', :on_error => 'cando' do
# ...
end
If there is no :on_cancel present, ‘cando’ behaves like a ‘redo’.
on_error: cursor commands
From ruote 2.3.0 on, the on_error attribute accepts the same command as a cursor (in fact, it’s best used with a cursor).
cursor :on_error => 'rewind' do
# the cursor will rewind by itself if there is an error in its steps
step_one
step_two
step_three
end
or
cursor :on_error => 'jump to final_step' do
# the cursor will jump to the final_step in case of error
# (it'd be better if the final step itself weren't the source of the error)
step_one
step_two
step_three
final_step
end
All the cursor commands are valid. Read the description of the cursor expression for more information.
:on_cancel
on_cancel is used to point at a subprocess or a participant that should be invoked / receive a workitem in case a [segment of a] process gets cancelled.
pdef = Ruote.process_definition :name => 'aircraft carrier' do
cursor :on_cancel => 'decommission' do
concurrence do
participant 'naval team', :task => 'operate ship'
participant 'air team', :task => 'operate planes'
end
end
define 'decommission' do
concurrence do
participant 'naval team', :task => 'decom weapons'
participant 'air team', :task => 'decom aircrafts'
end
end
end
In this process, the aircraft is operated. Upon cancelling, the subprocess ‘decommission’ is triggered, where the teams get different missions.
Note that, unlike :on_error, when an expression inside an :on_cancel enabled expression is cancelled, that will not trigger the :on_cancel. For example, if the ‘operate planes’ activity is cancelled, that will not trigger ‘decommission’. The trigger will occur if the cursor or the whole process instance is cancelled.
(note as well that when a process get killed, its on_cancel attributes will not trigger)
:on_timeout
On top of this page figures the description of the :timeout attribute. The :on_timeout attribute is a complement. It indicates what to do (participant or subprocess) when the timeout does trigger.
Apart from the name of a subprocess or a participant, :on_timeout can also take the ‘redo’ or the ‘error’ value.
The ‘redo’ value indicates that on timeout, the flagged expression should get cancelled (along with any children it may have) and be re-applied.
The ‘error’ value forces the process into error upon timeout (whereas the default timeout behaviour is to resume the flow). A process segment in error is blocked and requires an admin interventation (see process administration).
sequence do
participant 'author'
participant 'reviewer', :timeout => '3d', :on_timeout => 'redo'
participant 'editor'
end
In this example, the reviewer will receive a fresh workitem every 3 days, until he replies by himself to the flow (which will resume to the editor participant).
(ruote 2.3.0 on) The :on_timeout attribute accepts the same pre-defined handlers as the :on_error attribute, “jump”, “rewind”, “undo”, “pass”, …
(ruote 2.3.0 on) The :on_timeout attribute accepts “cancel” and “cando” values like the :on_error attribute does.
:tag
The tag attribute is used to tag a segment of a process.
Ruote.process_definition do
sequence do
sequence :tag => 'phase 1' do
alice
bob
end
sequence :tag => 'phase 2' do
charly
david
end
end
end
These tags then appear in the process variables :
p engine.process(wfid).tags.keys
# => [ "phase 1" ]
In this way, :tag can be used to flag large segments of process instances. Eras, phases, chapter, … Name it how you want.
(ruote 2.1.12 will probably add a tags field to its workitems, that keeps track of the currently seen tags)
The :tag is used as well by the _redo and the undo (cancel) expression.
The cursor / repeat expressions can be tagged too, when the cursor/loop has to be manipulated from outside (of the cursor/loop).
:filter
Ruote 2.2.0 introduces a :filter attribute for expressions.
Most of the documentation for this attribute is found in the doc for the filter expression. Note however, that the filter expression in one way, while the attribute version is two-sided, ‘in’ and ‘out’ (reaching the expression, leaving it).
Ruote.process_definition do
set 'v:f' => {
:in => [
{ :fields => '/^private_/', :remove => true }
],
:out => [
{ :fields => '/^private_/', :restore => true },
{ :fields => '/^protected_/', :restore => true },
]
}
alpha
sequence :filter => 'f' do
bravo
charly
end
delta
end
In this example, the filter is placed in the variable named ‘f’. When the sequence after alpha is entered, the workitem fields whose name starts with “private_” are removed, bravo and charly can’t see them.
Once charly is done, the sequence terminates and the private fields are restored (like they were when reaching the bravo-charly sequence. The fields starting with ‘protected_’ are restored too, potentially overwriting changes made by bravo or charly.
There are different ways to pass filters.
Ruote.process_definition do
# directly
alpha :filter => {
:in => [
{ :fields => '/^private_/', :remove => true }
],
:out => [
{ :fields => '/^private_/', :restore => true },
{ :fields => '/^protected_/', :restore => true },
]
}
# via a variable
alpha :filter => 'f'
# via two variables
alpha :filter => { :in => 'f0', :out => 'f1' }
# as a participant
alpha :filter => 'p'
# as two participants
alpha :filter => { :in => 'p0', :out => 'p1' }
end
The format for filters passed directly or via variables as arrays is detailed for the filter expression. The ‘restore’ filter operation is particulary useful in the case of an ‘out’ (‘reply’) filter attribute.
Participants are registered in the engine like any other participant. There consume method is not expected to reply_to_engine(workitem)
class MyFilterParticipant
def consume(workitem)
return if workitem.fields['__filter_direction__'] == 'out'
# only filter when filter on 'out' ('reply') (vs 'in'/'apply')
workitem.fields.keys.each do |k|
workitem.fields.delete(k) if k.match(/^private_/)
end
end
end
engine.register_participant 'filter0', MyFilterParticipant
# ...
pdef = Ruote.process_definition do
alpha :filter => 'filter0'
end
The :filter attribute will favour the ‘filter’ method of the participant, if it has one. This method, unlike consume, will be expected to return the updated field hash (and it doesn’t receive the workitem, but the field hash directly).
class MyFilterParticipant
def filter(fields, direction)
return fields if direction == 'out'
fields.select { |k, v| ! k.match(/^private/) } # ruby 1.9.x !!!
end
end
:take and :discard
TODO
:timers
(introduced in ruote 2.3.0)
“alice receives a task, she has 15 days to do it, she’ll have to receive a reminder after 5 days and a final reminder after 12 days” can be conveyed as
Ruote.define do
# ...
alice :timers => '5d: reminder, 12d: final_reminder, 15d: timeout'
# ...
end
The pattern is “duration0: x, duration1: y, …, durationN: z”.
The “action” can be a participant name, a subprocess name or one of a set of keywords.
Those keywords are
- timeout : simply times out
alice :timers => '1h: timeout'
# is equivalent to
alice :timeout => '1h'
- error : after the given time, the expression is forced into an error
alice :timers => '1h: reminder, 12h: error'
What follows the “error” and precedes the end of the string or the next “,” (comma) is taken as the ‘error message’
alice :timers => '1h: reminder, 12h: error it went wrong'
- undo, pass
To forget alice and pass to bravo after 12 hours:
sequence do
alice :timers => '1h: reminder, 12h: undo'
bravo
end
- redo, retry
To force a re-application of an expression after a certain time:
alice :timers => '12h: redo'
# which is equivalent to
alice :timeout => '12h', :on_timeout => 'redo'
- skip, back, jump, rewind, continue, break, stop, over, reset
Those “commands” are understood as well (warning, they only apply if you are inside of a ‘cursor’ expression).
After twelve hours and no reply from alice, the flow will jump to participant charly (over participant bravo):
cursor do
alice :timers => '12h: jump charly'
bravo
charly
end
Note that sub-processes and participants that are triggered by :timers are “flanking”. When the expression holding the timers ends, they get cancelled (they don’t outlive the expression for which they are ‘timers’).
:scope
(introduced in ruote 2.3.0)
By default expressions inside of a workflow instance share the same variable scope. By setting the attribute :scope to “true”, a new variable scope is forced.
define 'flow' do
set 'v:v' => 'alice'
sequence :scope => true do
set 'v:v' => 'bob'
participant '${v:x}' # will deliver to bob
end
participant '${v:x}' # will deliver to alice
end
(This example can be reproduced by replacing “sequence :scope => true” by “let” which is a special alias of the sequence expression).
:await
(introduced in ruote 2.3.1)
The await attribute was added to ruote in order to help model directed graphs.
Imagine a case where you need to represent a graph of tasks where A and B execute concurrently, C executes as soon as A and B are over and D executes as soon as B is over.
With the await expression, this can be done as:
concurrence do
sequence :tag => 'ab' do
a
b :tag => 'b'
end
sequence do
await :left_tag => 'ab'
c
end
sequence do
await :left_tag => 'b'
d
end
end
This relies on the blocking behaviour of await when it has no nested expressions.
The :await attribute was introduced to simplify the notation above. The flow becomes:
concurrence do
sequence :tag => 'ab' do
a
b :tag => 'b'
end
sequence :await => 'left_tag:ab' do
c
end
sequence :await => 'left_tag:b' do
d
end
end
The sequences are made to wait for the event to happen before resuming.
The :await attribute understands the same language as the await expression, although it compacts it into something like “left_tag:ab”.
When given no prefix, the await attribute will consider the value as “left_tag:” thus, the above flow can be rewritten as:
concurrence do
sequence :tag => 'ab' do
a
b :tag => 'b'
end
sequence :await => 'ab' do
c
end
sequence :await => 'b' do
d
end
end
or, since await applies to any expression, it can be shortened to:
concurrence do
sequence :tag => 'ab' do
a
b :tag => 'b'
end
c :await => 'ab'
d :await => 'b'
end