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”, “undo”, “cancel”, … See below.
When it points to a subprocess, the branch in error gets replaced by an instance of that subprocess. When it points to a participant branch gets replaced by a single workitem despatchement to the participant.
Before running the subprocess or participant that is meant to handle the error, ruote places the error in a workitem field named “__error__”. The value of the field is a hash describing the error, with keys, like “fei”, “at”, “class”, “message”, “trace”, etc.
Ruote 2.3.0 introduces a dedicated “on_error” expression that gives some pattern matching ability to on_error. See on_error expression.
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
‘retry’ may be used instead of ‘redo’.
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
‘pass’ may be used instead of ‘undo’.
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
That piece of process will call the participant (or subprocess) “decommission” when cancelled and also when encountering an error.
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_error: store:x
(since ruote 2.3.1)
This on_error directive let’s the error segment cancel itself and then stores the error in the given workfitem field or process variable.
sequence :on_error => 'store: x' do # ... end
It can be used in conjunction with the “error :re => ‘$f:x’” to reraise the error later on. Or simply with an :if to run certain code if an error was encountered previously.
do_this :on_error => 'store: x' # store error in workitem field "x" do_this :on_error => 'store: f:x' # store error in workitem field "x" do_this :on_error => 'store: v:y' # store error in process variable "y"
“bury” can be used instead of “store”.
: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
These two attribute influence what workitem fields are carried from the expression they adorn.
They can be used to constrain the output of a segment of process.
:take acts as a whitelist:
alpha :take => 'a' # if the alpha participant or subprocess set any field in the workitem # it was given, only the field named "a" will be kept alpha :take => /^a/ # regular expressions are OK alpha :take => [ 'a', /^b/ ] # arrays are OK too
:discard acts as a blacklist:
bravo :discard => [ 'a', /^b/ ] # the field "a" and any field beginning with a "b" will get discarded
:discard => true discards any field set by the adorned expression:
bravo :discard => true
Can be useful to somehow silent a segment of process.
: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