Usually, a process definition weaves the trajectory of workitems among participants.
1 <process-definition name="def0" revision="0.1"> 2 <sequence> 3 <participant ref="accounting" /> 4 <!-- send invoice --> 5 <participant ref="logistics" /> 6 <!-- deliver items --> 7 </sequence> 8 </process-definition>
(note : this is not the only way to define processes)
Participants are invoked with the participant expression.
There are basically two different kind of participant, ‘local’ and ‘remote’ (or maybe ‘proxy’). Find there some notes about writing custom participants as well.
Participants are registered in the engine via the “register_participant” method of the engine :
1 require 'openwfe/engine/engine' 2 engine = Engine.new 3 4 require 'openwfe/participants/store_participants' 5 hpAlice = HashParticipant.new 6 7 hpBob = HashParticipant.new 8 9 engine.register_participant(:alice, hpAlice) 10 engine.register_participant(:bob, hpBob) 11 12 # ...
To remove participants from the engine :
1 engine.unregister_participant(:alice) 2 engine.unregister_participant("bob")
Participants can be categorized in two flavours : push and pull. Push participants like for example a MailParticipant or the BlockParticipant react immediately upon receiving a workitem (sending an email, executing some code).
Pull participants like store participants, simply keep the workitems for later consumption by external services.
Here are the participant implementations that ship with Ruote (OpenWFEru) :
Their descriptions :
ActiveParticipantIt’s too bad this participant comes first in this [alphabetical] list. It’s a bit advanced, and has been deprecated by the ArParticipant.
The active page is dedicated to the ActiveRecord items in Ruote.
ActiveStoreParticipantIt’s a refinement of the ActiveParticipant. This is the participant used by ‘densha’ for its workitem stores. Never releases of ruote-web2 uses the new ArParticipant, which has deprecated the ActiveStoreParticipant.
ActiveResourceParticipantRuby on Rails provides a REST oriented framework for interacting with its active record, it’s called Active Resource. Torsten Schoenebaum has written a participant that wraps HTTP interactions with Rails [active] resource.
1 engine.register_participant( 2 'reminder', 3 ActiveResourceParticipant.new( 4 :site => 'http://localhost:3000', 5 :resource_name => 'invoice', 6 :method => 'post', 7 :argument => 'invoice_details'))
This example will post (create) a new invoice based on the information (array) found in the field ‘invoice_details’ of incoming workitems.
AliasParticipantGiven a list of ‘user participant’ :
1 engine.register_participant :alice do |workitem| 2 workitem.whatever = "oh and by the way" 3 end 4 engine.register_participant :bob do |workitem| 5 workitem.decision = "there is a coordination problem, let's decide later" 6 end 7 engine.register_participant :charly do |workitem| 8 workitem.time_estimate = "Carlito says : cuando la vaca vuela" 9 end
Some aliases may get helpful :
1 engine.register_participant :boss, AliasParticipant.new("bob") 2 engine.register_participant :hr_head, AliasParticipant.new("alice") 3 engine.register_participant "engineering_.*", AliasParticipant.new("charly")
AtomFeedParticipantMore at atom :: AtomFeedParticipant
ArParticipantThe preferred participant for storing workitems in ActiveRecord. It was developed for ruote-web2 and is also the new default participant for ruote-rest from version 0.9.21. This participant deprecates the ActiveParticipant and ActiveStoreParticipant.
To migrate from the ActiveParticipant to the new ArParticipant you use the arparticipants.rb script.
The active page is dedicated to the ActiveRecord items in Ruote.
AtomPubParticipantMore at atom :: AtomPubParticipant
BlockParticipantThe participant with the most Ruby-feel.
The idea is to directly map a Ruby block as a participant to a workflow.
1 engine.register_participant(:vat) do |workitem| 2 price = Integer(workitem.price) 3 price = price + price * $vat 4 workitem.net_price = price 5 end
The reply to the engine is implicit, it has not to be stated in the block.
This participant is used in one of the examples in the examples/ folder of Ruote.
In that example, it’s barely used to dump the incoming workitem to the standard output (stdout).
You can pass a block that accepts no arguments, one (workitem) or two (flow expression and workitem) to this participant.
The last value in the block (its return value) is set as the value of the field ‘result’ of the outgoing workitem. Beware : do not set this value to something not serializable (like a reference to a complex object or something like that).
1 engine.register_participant(:my_participant) do |flow_expression, workitem| 2 workitem.attributes["parent"] = flow_expression.parent_id.to_s 3 # 4 # just storing the parent expression of the participant expression 5 # that sent the workitem, as a string in the workitem 6 # 7 # you could do more complicated and dangerous things with 8 # handle on the flow expression (but that's an advanced topic) 9 end
Note that if the block’s execution could take a certain amount of time, it’d be better to provide error handling and CancelItem handling inside of it. A short and hypothetical example :
1 engine.register_participant "sluggy" do |workitem| 2 3 if workitem.is_a?(OpenWFE::CancelItem) 4 # 5 # cancel job 6 7 job_id = jobs[workitem.fei] 8 9 external_sluggy_system.cancel_job(job_id) 10 else 11 # 12 # launch job 13 14 job_id = external_sluggy_system.execute_job( 15 workitem.param0, workitem.param1) 16 17 jobs[workitem.fei] = job_id 18 end 19 end
Maybe in such a case, a dedicated participant class could be better, leaving BlockParticipant for tiny chunk of ruby code.
BlogParticipantMore at atom :: BlogParticipant
CsvParticipantCsvParticipants are called “decision tables” or “decision participants” in Java OpenWFE.
Here is a blog entry about the CSV tables behind decision participants.
Note that since Ruote 0.9.13, the CSVParticipant has been moved to the OpenWFE::Extras namespace.
rdoc (CSV table)
FileParticipantThis participant just dumps workitems to files. By default, it dumps the workitem as a YAML string, but you can override the encode_workitem() method.
1 require 'openwfe/participants' 2 3 engine.register_participant("Fedor", OpenWFE::FileParticipant)
In this example, workitems for participant ‘Fedor’ will get dispatched (dumped) in the directory ./work/out/
The FileParticipant has a counterpart : the FileListener which polls directory for incoming ‘file’ workitems.
The FileParticipant alone is interesting as well for debugging a business process, it can be placed at special places to “dump” the workitem as it reaches those points.
HashParticipantIt directly maps a participant with a hash for storing workitems waiting for treatment.
This participant is used in one of the examples in the examples/ folder of Ruote.
But the better rdoc to browse is the one of the StoreParticipantMixin module HashParticipant implements.
JabberParticipantKenneth Kalmer implemented this participant for his ruote engines to communicate with agents over XMPP (Jabber).
1 engine.register_participant( 2 :jabber, 3 OpenWFE::Extras::JabberParticipant.new( 4 :jabber_id => 'ruote@devbox', 5 :password => 'secret', 6 :contacts => [ 'kenneth@devbox', 'torsten@devbox' ]))
Kenneth also contributed a JabberListener for Ruote to listen to message (workitems) coming over XMPP (rdoc)
MailParticipantThis participant sends a notification per email. The text of the email is generated by filling a template with the data found in the workitem payload.
(Thanks to Alain Hoang for the initial implementation of this participant)
rdoc (a former version of the MailParticipant)
NoOperationParticipantImmediately replies to the engine, doesn’t touch the workitem (no operation).
NullParticipantDiscards the workitem (no operation), but doesn’t reply to the engine. A bit like a BPM /dev/null
ProcessParticipantThe classical way to call a participant is via the subprocess :
1 <sequence> 2 <participant ref="alice" /> 3 <subprocess ref="review" /> 4 <participant ref="bob" /> 5 <subprocess ref="review" /> 6 <subprocess ref="http://process.server.company.org/processes/proc0.xml" /> 7 </sequence> 8 9 <process-definition name="review"> 10 <loop> 11 <participant ref="boss1" /> 12 <participant ref="boss2" /> 13 <break if="${f:boss2_decision} == ok" /> 14 </loop> 15 </process-definition>
With a Ruby process definition it would look like :
1 sequence do 2 alice 3 review 4 bob 5 review 6 subprocess :ref => "http://process.server.company.org/processes/proc0.xml" 7 end 8 9 process_definition :name => "review" do 10 _loop do 11 boss1 12 boss2 13 _break :if => "${f:boss2_decision} == ok" 14 end 15 end
Note that the participant and the subprocess are simply referred directly via their names, except for the subprocess call using an URL. (Note too that expressions whose names are Ruby keywords like ‘loop’ and ‘break’ are escaped with an underscore as a prefix.
A ProcessParticipant can solve this URL problem :
1 require 'openwfe/participants' 2 3 engine.register_participant( 4 "proc0", 5 ProcessParticipant.new("http://process.server.company.org/processes/proc0.xml"))
The process definition body would then look like :
1 sequence do 2 alice 3 review 4 bob 5 review 6 proc0 7 end
rdoc for more information on how to register a participant (as a URL, as a Ruby process class, …)
SoapParticipantWraps a call to a webservice within a participant. The ‘result’ field of the workitem receives then the result of the call.
This is a minimal implementation, but it’s Ruby, the extension possibilities are huge.
This participant is used in one of the examples in the examples/ folder of Ruote.
SqsParticipantSQS is an Amazon web service : ‘Amazon Simple Queue Service (Amazon SQS) offers a reliable, highly scalable hosted queue for storing messages as they travel between computers’, learn more at http://aws.amazon.com
The SqsParticipant deposits workitems on a SQS queue where they can be read by other applications.
1 require 'openwfe/extras/participants/sqs_participants' 2 3 engine.register_participant(:queue, OpenWFE::Extras::SqsParticipant.new("workqueue0"))
By default, this participant does a YAML dump of the workitem in its ‘hash version’ and then encodes it as Base64 before putting it in the queue.
You can very easily change that by overriding encode_workitem(), for example :
1 require 'openwfe/extras/participants/sqs_participants' 2 3 sp = OpenWFE::Extras::SqsParticipant.new("workqueue0") 4 class << sp 5 # 6 # instead of extending SqsParticipant, we simply modify the 7 # method implementation just for that sp instance 8 # 9 def encode_workitem (wi) 10 msg = "" 11 msg << wi.fei.to_s 12 msg << "\n" 13 msg << wi.attributes["task_description"] 14 msg << "\n" 15 msg 16 end 17 end 18 19 engine.register_participant(:queue, sp)
There is a counterpart to the SqsParticipant : an SqsListener
1 require 'openwfe/extras/listeners/sqslisteners' 2 3 engine.add_workitem_listener( 4 OpenWFE::Extras::SqsListener.new(:wiqueue3, engine.application_context), 5 '2m15s') 6 # 7 # will poll our SQS queue named "wiqueue3" for workitems 8 # every 2 minutes and 15 seconds
A listener allows InFlowWorkItems to resume back in their originating process instance. They also accept (if allowed) LaunchItems and launch a brand new process instance according to the info found in the LaunchItem.
The SqsListener expects by default that workitems (InFlowWorkItem and LaunchItem instances) are encoded as hashes, then dumped via YAML, then encoded with Base64 (like SqsParticipant does).
TwitterParticipantThe TwitterParticipant was first documented in this blog post on the tech blog.
Twitter notification could be a lightweight substitute for email notification.
1 require 'openwfe/extras/participants/twitter_participants' 2 3 tp = OpenWFE::Extras::TwitterParticipant.new( 4 twitter_account, twitter_password, :no_ssl => true) 5 # 6 # remove this :no_ssl param if the Participant should use SSL 7 # (as sometimes Twitter doesn't listen on SSL) 8 9 engine.register_participant("twitter_notification", tp)
The TwitterParticipant, out of the box supports status and direct messages. The message itself is the value of the “twitter_message” workitem field. To send a direct message, set the “twitter_target” workitem field to a twitter login name and the twitter user will receive the message via email, thanks to Twitter (email notification anyway).
The way the message and the target are determined can be changed by overriding (or monkey patching) the extract_message() method. Here is an example :
1 require 'openwfe/extras/participants/twitter_participants' 2 3 class MyTwitterParticipant < OpenWFE::Extras 4 5 # 6 # This method is meant to return a pair login name / message. 7 # 8 def extract_message (workitem) 9 10 message = "" 11 message << "customer '#{workitem.customer_name}'" 12 message << " replied '#{workitem.customer_reply}'" 13 message << " (#{Time.now.to_s})" 14 15 [ nil, message ] 16 # 17 # always returning nil as the twitter target, the message 18 # will thus always be a status message, not a direct message 19 end 20 end
YamlParticipantThis participant is an extension of the HashParticipant built on top of a YAML file persistence.
By default, the workitems are stored under “./work/participants/name_of_the_participant/”
This participant is used in the sample code of the ManoTracker
As for the HashParticipant, it’s interesting to browse the rdoc of the StoreParticipantMixin module that this participant implements.