Ruote for multi-step form

56 views
Skip to first unread message

Reed Law

unread,
Dec 12, 2011, 10:11:21 AM12/12/11
to ruote
We're using Ruote for managing workflow between various participants,
but now I'm thinking about how to use it to implement a multi-step
form for one participant at a time.

Currently we have a form of around 10 steps filled in by an author.
Because there are no other participants involved, this happens outside
of the workflow. The steps are stored in the model in the field
"current_step", and 2 arrays (steps, and completed_steps). Once the
author completes the form the workflow is launched and sent out to
several participants. Now I'm thinking I want to use Ruote to manage
the steps for coauthors instead of attaching them to the model. With
Ruote I can easily store steps in workitem fields. If I used the
model, there would have to be a child step object for each coauthor.

So my question is: has anyone implemented a multi-step form with
Ruote? I want to display a checklist for each participant. The
checklist items should be marked completed once done. So my Rails
controller/views will need to somehow access the process branches.

For example:

concurrent_iterator :on => '${f:coauthors}', :tag =>
'coauthor_forms', :to_var => 'v' do
concurrence do
participant 'coauthor-${v:v}', :task =>
'complete_information'
participant 'coauthor-${v:v}', :task => 'submit_copyright'
participant 'coauthor-${v:v}', :task =>
'submit_disclosure'
end
end

This is nearly good enough, but once a workitem is complete, there is
no "rewind". I also looked at the "iterator" expression, but since
those aren't executed in parallel, they will disappear from the views
once they are finished. Ideally, each item on the checklist could be
revisited until the final "submit".

John Mettraux

unread,
Dec 12, 2011, 11:26:18 AM12/12/11
to openwfe...@googlegroups.com

On Mon, Dec 12, 2011 at 07:11:21AM -0800, Reed Law wrote:
>
> (...)

>
> So my question is: has anyone implemented a multi-step form with
> Ruote? I want to display a checklist for each participant. The
> checklist items should be marked completed once done. So my Rails
> controller/views will need to somehow access the process branches.
>
> For example:
>
> concurrent_iterator :on => '${f:coauthors}', :tag =>
> 'coauthor_forms', :to_var => 'v' do
> concurrence do
> participant 'coauthor-${v:v}', :task =>
> 'complete_information'
> participant 'coauthor-${v:v}', :task => 'submit_copyright'
> participant 'coauthor-${v:v}', :task =>
> 'submit_disclosure'
> end
> end
>
> This is nearly good enough, but once a workitem is complete, there is
> no "rewind". I also looked at the "iterator" expression, but since
> those aren't executed in parallel, they will disappear from the views
> once they are finished. Ideally, each item on the checklist could be
> revisited until the final "submit".

Hello Reed,

welcome to the ruote mailing list.

I'm not sure I understood correctly, let me suggest some solutions and you'll
correct me.

I guess you're using "concurrence" because using the cursor would be too
slow (though it has a rewind) ?

---8<---


concurrent_iterator :on => '${f:coauthors}', :tag => 'coauthor_forms', :to_var => 'v' do

cursor do


participant 'coauthor-${v:v}', :task => 'complete_information'
participant 'coauthor-${v:v}', :task => 'submit_copyright'
participant 'coauthor-${v:v}', :task => 'submit_disclosure'
end
end

--->8---

What about doing the multi-step in your web framework ?

---8<---


concurrent_iterator :on => '${f:coauthors}', :tag => 'coauthor_forms', :to_var => 'v' do

participant 'coauthor-${v:v}', :task => 'complete_and_submit'
end
--->8---

The 3 forms would be dealt with the same workitem session. (Sorry, I know it
means more work for you).

In such cases, I tend to use something like quaderno:

https://github.com/jmettraux/quaderno/

Where I have one tab per "checklist":

http://ruote.rubyforge.org/quaderno/readme_0.html

The participant receives one workitem but the it contains multiple form
units.


What do you think ?

--
John Mettraux - http://lambda.io/processi

Reed Law

unread,
Dec 12, 2011, 12:07:45 PM12/12/11
to ruote
Hi John,

Thanks for the welcome but I've posted here before (as R Law).

You understood me correctly for the most part. The problem with the
cursor is that each participant would only receive one workitem at a
time, but I wanted to hand off several (one for each step in the
checklist).

Your suggestion works and I'll probably go with something like that.
Quarderno looks nice but we've already got UI elements for checklists.

A question that came up as I looked into this was: how do you
implement a participant that replies immediately if some condition is
met? I know I could use the if expression in the process definition,
but it seems like that is only suitable for testing equality. I want
to check if some data is present in an external service. If the
information is present I want to copy it over (#1); if not, the
coauthor needs to complete a form (#2). So the participant
implementation will first attempt #1, and if it's successfully, simply
reply to the workitem (proceed). If #1 fails, the participant should
work as a normal storage participant for #2. Is this possible?

Thank you,
Reed

John Mettraux

unread,
Dec 13, 2011, 7:03:51 AM12/13/11
to openwfe...@googlegroups.com

On Mon, Dec 12, 2011 at 09:07:45AM -0800, Reed Law wrote:
>
> Thanks for the welcome but I've posted here before (as R Law).

This post was moderated so I quickly assumed you were new.

> You understood me correctly for the most part. The problem with the
> cursor is that each participant would only receive one workitem at a
> time, but I wanted to hand off several (one for each step in the
> checklist).

Your interface could aggregate those distinct workitems, their feis (#fei)
being quite close (same wfid, same expid root).

> Your suggestion works and I'll probably go with something like that.
> Quarderno looks nice but we've already got UI elements for checklists.

> A question that came up as I looked into this was: how do you
> implement a participant that replies immediately if some condition is
> met? I know I could use the if expression in the process definition,
> but it seems like that is only suitable for testing equality.

With the upcoming ruote 2.2.1, it can do better than testing equality.

https://github.com/jmettraux/ruote/blob/master/test/unit/ut_6_condition.rb

> I want to check if some data is present in an external service. If the
> information is present I want to copy it over (#1); if not, the
> coauthor needs to complete a form (#2). So the participant
> implementation will first attempt #1, and if it's successfully, simply
> reply to the workitem (proceed). If #1 fails, the participant should
> work as a normal storage participant for #2. Is this possible?

What about making those steps explicit in the process definition ?

---8<---
sequence do
copy_info_over
human :unless => '${info}'
end
--->8---

If you really want to pack that into one participant you could do something
like:

---8<---
class Acme::StorageParticipant < Ruote::StorageParticipant

# Returns false so that the on_workitem operations are performed in
# a new thread, not blocking the worker.
#
def do_not_thread

false
end

def on_workitem

workitem.fields['copying'] = true
super

info = ExternalService.copy_info_over

wi = by_fei(fei) # re-fetch

return unless wi
#
# workitem gone, probably cancelled, work over.

if info
#
# information was fetched successfully, let's proceed
#
wi.fields['info'] = info
proceed(wi)
else
#
# re-put with updated info
#
# this will fail silently if the workitem is gone, not much to do
# anyway
#
wi.fields['copying'] = false
@context.storage.put(doc)
end
end

# no need to override #on_cancel
end
--->8---

I hope it helps.


Best regards,

R Law

unread,
Dec 13, 2011, 10:28:46 AM12/13/11
to ruote
> What about making those steps explicit in the process definition ?
>
> ---8<---
> sequence do
>   copy_info_over
>   human :unless => '${info}'
> end
> --->8---

Is "copy_info_over" a participant? If so I still have to implement a
custom participant, right? Is there any advantage to having many
participants that handle small tasks over having fewer that handle
bigger tasks?

R Law

unread,
Dec 13, 2011, 11:36:48 AM12/13/11
to ruote
I'm trying your first suggestion but I'm not sure how to implement it:

concurrent_iterator :on => '${f:coauthor_emails}', :tag =>


'coauthor_forms', :to_var => 'v' do
concurrence do
participant 'coauthor-${v:v}', :task =>

'complete_information', :unless => '${v:v}_info_present'


participant 'coauthor-${v:v}', :task => 'submit_copyright'
participant 'coauthor-${v:v}', :task => 'submit_disclosure'
end
end

RuoteKit.engine.launch(self.pdef, {'coauthor_emails' =>
'f...@bar.com,te...@example.com', 'te...@example.com_info_present' =>
'true' })

With this code, both f...@bar.com and te...@example.com skip over the
"complete_information" task. I need the :unless to check if this
particular user's information is present.

On Dec 13, 7:03 am, John Mettraux <jmettr...@gmail.com> wrote:
> On Mon, Dec 12, 2011 at 09:07:45AM -0800, Reed Law wrote:
>
> > Thanks for the welcome but I've posted here before (as R Law).
>
> This post was moderated so I quickly assumed you were new.
>
> > You understood me correctly for the most part. The problem with the
> > cursor is that each participant would only receive one workitem at a
> > time, but I wanted to hand off several (one for each step in the
> > checklist).
>
> Your interface could aggregate those distinct workitems, their feis (#fei)
> being quite close (same wfid, same expid root).
>
> > Your suggestion works and I'll probably go with something like that.
> > Quarderno looks nice but we've already got UI elements for checklists.
> > A question that came up as I looked into this was: how do you
> > implement a participant that replies immediately if some condition is
> > met? I know I could use the if expression in the process definition,
> > but it seems like that is only suitable for testing equality.
>
> With the upcoming ruote 2.2.1, it can do better than testing equality.
>

>  https://github.com/jmettraux/ruote/blob/master/test/unit/ut_6_conditi...

John Mettraux

unread,
Dec 13, 2011, 7:18:28 PM12/13/11
to openwfe...@googlegroups.com

On Tue, Dec 13, 2011 at 07:28:46AM -0800, R Law wrote:
>
> > What about making those steps explicit in the process definition ?
> >
> > ---8<---
> > sequence do
> > � copy_info_over
> > � human :unless => '${info}'
> > end
> > --->8---
>
> Is "copy_info_over" a participant?

Hello,

it could be participant or a subprocess. To make it more definite:

---8<---
sequence do
� participant 'copy_info_over'
� participant 'human' :unless => '${info}'
end
--->8---

> If so I still have to implement a
> custom participant, right?

Yes.

> Is there any advantage to having many
> participants that handle small tasks over having fewer that handle
> bigger tasks?

Non-exhaustive lists:

Advantages:

- can be reused in multiple process definitions
- simpler to implement (easier to think about how to deal with cancel calls)

Disadvantages:

- process definition gets too detailed (but subprocesses can be used to keep
the flow readable)

John Mettraux

unread,
Dec 13, 2011, 7:24:55 PM12/13/11
to openwfe...@googlegroups.com

On Tue, Dec 13, 2011 at 08:36:48AM -0800, R Law wrote:
>
> concurrent_iterator :on => '${f:coauthor_emails}', :tag =>
> 'coauthor_forms', :to_var => 'v' do
> concurrence do
> participant 'coauthor-${v:v}', :task =>
> 'complete_information', :unless => '${v:v}_info_present'
> participant 'coauthor-${v:v}', :task => 'submit_copyright'
> participant 'coauthor-${v:v}', :task => 'submit_disclosure'
> end
> end
>
> (...)

>
> With this code, both f...@bar.com and te...@example.com skip over the
> "complete_information" task. I need the :unless to check if this
> particular user's information is present.

Hello,

you can nest dollar expressions:

---8<---


concurrent_iterator :on => '${f:coauthor_emails}', :tag => 'coauthor_forms', :to_var => 'v' do
concurrence do

participant 'coauthor-${v:v}', :task => 'complete_information', :unless => '${${v:v}_info_present}'


participant 'coauthor-${v:v}', :task => 'submit_copyright'
participant 'coauthor-${v:v}', :task => 'submit_disclosure'
end
end

--->8---

Your version always resolved to something like:

participant 'coauth...@bar.com', :unless => 'f...@bar.com_info_present'

which got skipped since an non-empty string is considered true.

The nested version should resolve (second to last resolution) to:

participant 'coauth...@bar.com', :unless => '${f...@bar.com_info_present}'

Which will get skipped if the workitem field 'f...@bar.com_info_present' holds
a trueish value.

Reed Law

unread,
Dec 14, 2011, 2:34:18 PM12/14/11
to ruote
> Hello,
>
> you can nest dollar expressions:
>
> ---8<---
> concurrent_iterator :on => '${f:coauthor_emails}', :tag => 'coauthor_forms', :to_var => 'v' do
>   concurrence do
>     participant 'coauthor-${v:v}', :task => 'complete_information', :unless => '${${v:v}_info_present}'
>     participant 'coauthor-${v:v}', :task => 'submit_copyright'
>     participant 'coauthor-${v:v}', :task => 'submit_disclosure'
>   end
> end
> --->8---

With this code the dollar notation doesn't seem to get evaluated. I
get this as the params for the coauthor workitems:
"coauthor-
f...@bar.com":null,"task":"complete_information","unless":""

And the workitem is not skipped if I launch the process like so:
coauthor_emails = "f...@bar.com,f...@baz.com"
RuoteKit.engine.launch(self.pdef, {'coauthor_emails' =>
coauthor_emails, "f...@bar.com_info_present" => true})

If I leave out the dollar notation in the process definition and
use :unless => 'f...@bar.com_info_present', the task is skipped for
every participant.

Can you make out if I'm doing anything else wrong?

John Mettraux

unread,
Dec 14, 2011, 8:32:17 PM12/14/11
to openwfe...@googlegroups.com

On Wed, Dec 14, 2011 at 11:34:18AM -0800, Reed Law wrote:
>
> With this code the dollar notation doesn't seem to get evaluated. I
> get this as the params for the coauthor workitems:
> "coauthor-
> f...@bar.com":null,"task":"complete_information","unless":""
>
> And the workitem is not skipped if I launch the process like so:
> coauthor_emails = "f...@bar.com,f...@baz.com"
> RuoteKit.engine.launch(self.pdef, {'coauthor_emails' =>
> coauthor_emails, "f...@bar.com_info_present" => true})
>
> If I leave out the dollar notation in the process definition and
> use :unless => 'f...@bar.com_info_present', the task is skipped for
> every participant.
>
> Can you make out if I'm doing anything else wrong?

Hello,

you're not doing anything wrong.

There's just a little known feature of the dollar notation:

---8<---
require 'rubygems'
require 'ruote'

engine = Ruote::Engine.new(Ruote::Worker.new(Ruote::HashStorage.new))

pdef = Ruote.process_definition do
echo '${hash.foo.bar}' # => 'baz'
echo '${nada.foo.bar}' # => ''
end

engine.noisy = true

wfid = engine.launch(
pdef,
'hash' => { 'foo' => { 'bar' => 'baz' } },
'nada.foo.bar' => 'nada')

engine.wait_for(wfid)
--->8---

In your case, it thinks "f...@bar.com_info_present" is about something like
{ 'foo@bar' => { 'com_info_present' => '...' } }

Here is something that works (ruote 2.2.1 at least):

---8<---
require 'rubygems'
require 'ruote'

engine = Ruote::Engine.new(Ruote::Worker.new(Ruote::HashStorage.new))

pdef = Ruote.process_definition do

concurrent_iterator :on => '$f:coauthors', :to_field => 'ca' do
#
# using "$f:x" instead of "${f:x}" so that actual array is used
# instead of string representation of the array...

concurrence do
participant 'coauthor-${ca.id}', :task => 'complete_information', :unless => '${${ca.id}_info_present}'
participant 'coauthor-${ca.id}', :task => 'submit_copyright'
participant 'coauthor-${ca.id}', :task => 'submit_disclosure'
end
end
end

class CoAuthor
include Ruote::LocalParticipant

def consume(wi)
puts [
wi.fei.expid, wi.participant_name,
wi.params['task'], wi.fields['ca']['email']
].join(' ')
reply_to_engine(wi)
end
end

engine.register do
participant 'coauthor-.+', CoAuthor
end

#engine.noisy = true

wfid = engine.launch(
pdef,
'coauthors' => [
{ 'id' => 'bar', 'email' => 'f...@bar.com' },
{ 'id' => 'baz', 'email' => 'f...@baz.com' }
],
'bar_info_present' => true)

engine.wait_for(wfid)
--->8---


Sorry for the confusion, best regards,

John Mettraux

unread,
Dec 14, 2011, 8:45:46 PM12/14/11
to openwfe...@googlegroups.com
Hello,

this works too with arrays:

---8<---
concurrent_iterator :on_field => 'coauthors', :to_field => 'ca' do
--->8---

Best regards,

R Law

unread,
Dec 19, 2011, 8:04:37 PM12/19/11
to ruote
I got it working like this:

concurrent_iterator :on_field => 'coauthors', :to_field =>

'ca', :tag => 'coauthor_forms' do
concurrence do
participant 'coauthor-${ca}', :task =>
'complete_information', :unless => '${ca}_info_present'
participant 'coauthor-${ca}', :task => 'submit_copyright'
participant 'coauthor-${ca}', :task => 'submit_disclosure'
end
end

Then I pass an array of coauthor emails into the params. Since email
addresses are much easier to read than IDs, I chose to remove the
extra dollar notations.

It's working beautifully now. Thanks so much for the help (and of
course the excellent work on creating this workflow engine in the
first place)!

Reply all
Reply to author
Forward
0 new messages