-
Notifications
You must be signed in to change notification settings - Fork 50
Home
Workflowable is a gem that allows easy creation of workflows in Ruby on Rails applications. Workflows can contain any number of stages and transitions, and can trigger customizable automated actions as states are triggered.
This section will walk through the steps for installing and setting up workflowable.
To your application's Gemfile, add:
gem 'workflowable'
Run the bundle command, install the migrations, and migrate your database:
bundle install
rake workflowable:install:migrations
rake db:migrate
In order to create/modify workflows through the web interface, you'll need to add a entry to your routes.rb file:
mount Workflowable::Engine => "/workflowable", as: :workflowable
This will allow the workflowable interface to be accessed at /workflowable
If you'd like to restrict access to the workflowable interface (recommended) one way you can do this is to add a constraint to your routes file. For example, if your user model has a "admin?" method, you could do the use the following instead of the example above:
admin_constraint = lambda do |request|
request.env['warden'].authenticate? && request.env['warden'].user.admin?
end
constraints admin_constraint do
mount Workflowable::Engine => "/workflowable", as: :workflowable
end
Workflows need a model to attach to. For example, maybe you're associating a workflow with a Ticket model. In order to do this, add the following like to the model:
acts_as_workflowable
This section will give a brief overview of the concepts used by the Workflowable gem.
Workflow: A process, generally with multiple, ordered stages
Stage: A state within the process
Initial stage: The state the workflow starts in
Action: A function that gets run as part of adding a workflow to an item or moving between stages
Before action: An action that gets run when transitioning into a specific stage
After action: An action that gets run when transitioning out of a specific stage
Global action: An action that gets run when between any stage, including moving into the initial stage (i.e. when a result is first flagged)
This section will walkthrough the steps needed to create a basic workflow.
- Go to the workflowable admin section (/workflowable)
- Click "New Workflow"
- Give your workflow a name
We're going to create 4 stages for our workflow: "New", "Investigating", "Complete", and "Not an Issue". While still on the new workflow page (from Step 3 above).
- Click "Add a Stage"
- Enter "New" for the name of the stage
- Repeat for each Stage clicking "Add a Stage" and entering the stage's name.
- When all stages have been added, click "Create Workflow"
After you click "Create Workflow" you will be atken to a page to set up the allowed transitions between stages. We're going to create a simple workflow that starts with "New", can move to "Investigating", and can then move to either "Complete" or "Not an Issue". We'll also setup the Workflow to allow "Not an Issue" items to be transitioned back to "Investigating".
A workflow must have one initial stage. We'll set "New" as the initial stage.
- Use the "Initial Stage" dropdown to select "New".
Except the initial stage, each other stage need to be configure as a potential "next stage" or it won't be possible to transition to it. Let's setup our transitions as described above.
- Under "New" click "Add a next step"
- In the dropdown select "Investigating"
- Under "Investigating" click "Add a next step"
- In the dropdown select "Complete"
- Again under "Investigating" click "Add a next step"
- In the dropdown select "Not an Issue"
- Under "Not an Issue" click "Add a next step"
- In the dropdown select "Investigating"
- Click "Update Workflow"
At this point you have a fully functional workflow! On the view workflow page you should see a diagram showing the stages and transitions that you configured.
You can optionally add actions to be run when transitions between stages.
So for example, imagine we're we want the following behavior:
- For all workflow transition (eg. "New" to "Investigating" to "Closed", "Investingating" to "Not an Issue", etc.) send out a notification. This will be a Global Action.
- When an item is moved into the "Investigating" stage, we want to choose an assignee. This will be a Before Action.
- When an item is moving out of the "Investigating" stage, we want the user to add a comment that specifies their findings. This will be an After Action.
In order to follow these steps, you will need to have added the three files discussed in the "Advanced: Develop and Add Actions" section above.
- From the workflow list page (/workflowable) click edit next to our workflow.
- To create the global workflow, under Global Actions click "Add global action"
- Give the action the name "Notification Action"
- In the position field input 1. This specifies the order that the global actions will be run (were there more than 1!)
- In the action dropdown select "Nofitication Action"
Two fields should appear: "Specify Value: message (required)" and "Default Value: message". The "Specify Value" field allows you to specify, from this interface, what value should be passed in. This allows will ensure the value you specify is alwasy passed as a parameter when the action is run.
The "Default Value" field allows you to specify a default value for the parameter, which may be overriden by the when the transition is run.
- In the "Specify Value" field, type "This is the notification!"
Next let's setup the Before Action that would allow choosing an assignee for the item before moving it into the "Investigating" stage.
- Under the Investgating stage, click "Add before action" under Before Actions
- Give the action the name "Assignee Action"
- In the position field input 1
- In the action dropdown select "Assignee Action"
For this action's parameter, let's set a default value:
- In the "Default Value" field, type "John"
This would cause the default assingee to be "John", but this could be overriden by the user.
Finally, let's setup the After Action that would force the user to add a comment when moving ending the "Investigating" stage.
- Under the Investgating stage, click "Add after action" under After Actions
- Give the action the name "Comment Action"
- In the position field input 1
- In the action dropdown select "Comment Action"
We want the user to specify their own comment, so we'll leave both the "Specify Value" and "Default Value" fields blank.
Click "Update Workflow" to save your changes.
Now that we've setup a Workflow, we can walk through how we can use it to track the state of a ticket. For this section, we'll walk through the methods that can be used. Developing apppropriate controllers and forms will be left up to you!
In your web application, make sure you workflowable model contains "acts_as_workflowable". This will provide helper methods we need to use the workflow.
-
Open up the rails console
rails c
-
Create one of your workflowable objects. For example
ticket = Ticket.create(summary=>"Test ticket")
Currently this ticket will not be associated with a workflow. Let's add it to our test workflow.
-
Find the workflow you want to use:
# Find the first workflow, which is the one you created. Note the id and initial_stage_id. Workflowable::Workflow.first # We'll assume the following output: # #<Workflowable::Workflow id: 1, name: "Test workflow", initial_stage_id: 1 ... >
-
Set the workflow id that we want to use
ticket.workflow_id = 1 # The id from above
Before we can save the ticket, we need to see if there are any required/optional parameters for the initial stage.
ticket.next_step_options(1) # The "1" here is the initial stage id from above
This will return a hash with a list of parameters available/required for global, before, and after actions. This is an example of what you might see
{:global=>
{"Notification Action"=>
{"message"=>
{"required"=>true,
"type"=>:text,
"description"=>"The contents of the message",
"value"=>"This is the notification"
}
}
},
:before=>{},
:after=>{}
}
Here we have a hash, with the parameter requried for global, before, and after actions along with some metadata that helps us determine what to pass in. In this case, we can see that the Notification Action has a message parameter which is required. But, in the hash, there is a value set. This means the admin specified the value, so we don't have to do it now. If we tried to specify the value, we would not be allowed to override it.
If the value had been specified as a default value, it would look like this: {:global=> {"Notification Action"=> {"message"=> {"required"=>true, "type"=>:text, "description"=>"The contents of the message", "default"=>"This is the notification" } } }, :before=>{}, :after=>{} }
In this case, we could specify a value to override the default if we wanted.
Since we don't need to specify a value we can just save our ticket and it will perform our actions and set the stage to the initial stage:
ticket.save
# #<Ticket id: 1, stage_id: 1, workflow_id: 1, ... >
We can get more details if we want. For example let's see the details of the current stage.
ticket.stage
# #<Workflowable::Stage id: 1, name: "New", workflow_id: 1, created_at: "2014-08-18 04:20:39", updated_at: "2014-08-18 04:23:45">
Let's check what stages we can move to next.
ticket.stage.next_steps
# [#<Workflowable::Stage id: 2, name: "Investigating", workflow_id: 1, created_at: "2014-08-18 04:20:39", updated_at: "2014-08-18 04:20:39">]
So it looks like we are able to move from the current stage, "New", to "Investigating". This should sound familiar since it is how we setup our Workflow earlier!
Let's see what options we would need to use to move to the "Investigating" stage.
ticket.next_step_options(2)
You should get back a results like this:
{:global=>
{"Notification Action"=>
{"message"=>
{"required"=>true,
"type"=>:text,
"description"=>"The contents of the message",
"value"=>"This is the notification"
}
}
},
:before=>
{"Assignee action"=>
{"assignee"=>
{"required"=>true,
"type"=>:text,
"description"=>"This is the assignee",
"default"=>"John"
}
}
},
:after=>{}
}
Here we can see there is a global action (same as before) with an already specified parameter, as well as a before action that needs and an assignee with a default option provided. Let's override this value and assigne the ticket to "Cindy" instead. First let's confirm that we need to pass in this parameter. We don't need to run this here, but it will confirm that there are no options that we MUST specify if it returns nil:
ticket.validate_actions(2)
# nil
Let's run this again, but this type let's override the default value of "John" with "Cindy". We'll pass a hash as the second parameter, in the exact same format as before with a value key specified in the assignee hash:
ticket.validate_actions(2, {before: {"Before action notification" =>{message: { value: "Cindy"}}}} )
# nil
Look like this is ok. As you can see, we have the same hash format as the next_step_options method return. We removed all the extra key/values, but we could have passed them as well and they would have been ignored.
Since everything looks good, let's move to the next stage with these options.
ticket.set_stage(2, {before: {"Before action notification" =>{message: { value: "Cindy"}}}} )
# true
ticket
# #<ResultFlag id: 1, stage_id: 2, workflow_id: 1, ...
ticket.stage
# #<Workflowable::Stage id: 2, name: "Investigating" ...
Looks good! We've moved to the investigating stage!
Let's apply this one more time to move the ticket to the "Complete"" status.
ticket.stage.next_steps
# [#<Workflowable::Stage id: 3, name: "Complete", ...>,
# #<Workflowable::Stage id: 4, name: "Not an Issue" ...>]
ticket.next_step_options(3)
#{:global=>
# {"Notification Action"=>
# {"message"=>
# {"required"=>true,
# "type"=>:text,
# "description"=>"The contents of the message",
# "value"=>"This is the notification"}}},
# :before=>{},
# :after=>
# {"Comment after action"=>
# {"comment"=>
# {"required"=>true,
# "type"=>:text,
# "description"=>"The contents of the message"}}}}
Here we notce that a "comment" parameter is required but not specified. Let's see what happens if we try to change stages without specifying that option.
ticket.set_stage(3)
# [{"Comment after action"=>["comment is required\n"]}]
We got a message indicating that we missed the required parameter. We could have also seen this if we ran the validation_actions method:
ticket.validate_actions(3)
# [{"Comment after action"=>["comment is required\n"]}]
Instead of nil, we get an array of error messages.
Ok, now we know what we need to pass in. Let's complete this ticket for real:
ticket.set_stage(3, {after: {"Comment after action" =>{comment: { value: "Complete!"}}}} )
# true
ticket
# #<ResultFlag id: 1, stage_id: 3, workflow_id: 1, ...
ticket.stage
# #<Workflowable::Stage id: 2, name: "Complete" ...
As you can see, workflowable provides all the information you need to build a workflow and move between stages with custom actions at each step. All you have left to do is build the appropriate forms and controls to allow navigating the workflow!
By adding "acts_as_workflowable" to a class, Workflowable will add in several methods that can be used to set the stage, determine next steps, determine required options, validate options, etc. This section will discuss the methods available and their use.
This section will disucss the methods inherited by models that include "acts_as_workflowable"
This will return the Workflowable::Stage object for the object's current stage.
None
The object's current stage (Workflowable::Stage)
ticket.stage # Assume ticket is an instance of an acts_as_workflowable model
# #<Workflowable::Stage id: 2, name: "Investigating", workflow_id: 1,
By retrieving the object's current stage we can then call next_steps to determine which stages the object could move into next.
None
An array with possible next stages (ArrayWorkflowable::Stage)
ticket.stage.next_steps # Assume ticket is an instance of an acts_as_workflowable model
# [#<Workflowable::Stage id: 3, name: "Complete"...>, #<Workflowable::Stage id: 3, name: "Not an Issue" ...>, ...]
Returns an options hash to indicate which options are available and/or required to change to the indicated stage.
next_step (integer) The id of the next stage to move to
options (options hash, optional) The options the user has specified so far
user (devise user object, optional) The user making the change
The object's current stage (Workflowable::Stage)
ticket.next_step_options(3, {}, current_user) # Assume ticket is an instance of an acts_as_workflowable model
# { :global=>{}, :before=>{}, :after=>{}} # No options available
Validates whether, with the provided options we are able to move to the next stage. This provides a mechanism for checking the options before actually changes stages. This is useful if options may be dynmaic. For example, imagine an action will create a ticket in an externally ticketing system. If the "Project" we're creating the ticket in changes (because the user changes that value), the options may be different. By using validate actions we can check whether the options are acceptable and, if not, present the user an error.
next_step (integer) The id of the next stage to move to
options (options hash, optional) The options the user has specified so far
user (devise user object, optional) The user making the change
Array of error messages from the actions.
key: Name of the action with invalid parameters
value: The message provided by the action
ticket.validate_actions(3, {}, current_user) # Assume ticket is an instance of an acts_as_workflowable model
# [{"Notification Action"=>["message is required\n"]}]
Move the object into the specified stage.
next_step (integer) The id of the next stage to move to
options (options hash, optional) The options the user has specified
user (devise user object, optional) The user making the change
On error, an array of error messages from the actions. On success "true".
key: Name of the action with invalid parameters
value: The message provided by the action
# Assume ticket is an instance of an acts_as_workflowable model
# Example 1: Fail because of missing required options
ticket.set_stage(3, {}, current_user)
# [{"Notification Action"=>["message is required\n"]}]
# Example 2: Success
ticket.set_stage(3, {before: {"Notification Action" =>{message: { value: "Hello"}}}}, current_user)
# true
In order to allow maximum flexibility, acts_as_workflowable allows Actions to define a schema of options that can/must be passed into the action as part of transitioning to a stage. This section will discuss the format of this data. Your application will need to be able to recognize that options are required (using the methods provided and discussed above) and prompt or send the appropriate values.
Definind options occurs in the implementation of the action. In other words, when an action is developed it will define a set of options that are available and/or required. This is done by creating a class function (self.option) which will return a properly formatted hash.
The options hash has the following attributes:
key: (symbol) A key used to identify the option value. Each option must have a unique key
value: (hash) This will contain information about the option
value[:description] (string) A description of the option
value[:required] (boolean) Is the option required
value[:default] (Any type) A hardcoded default value (can be overriden by workflow admin or user if allowed)
value[:type] (symbol) A type of value that should be passed in the option
value[:choices] (array of hashes with format {id: , value: }, optional)
Note: workflowable supports three types:
- :boolean
- :choice
- Any thing else
The value for type used impacts how the options will appear in the admin interface. Boolean options will use a checkbox, choice options will use a dropdown (with choices pulled from the :choices value), and anything else will result in a text box being displayed.
Other values for type can be used, however in the workflowable admin interface, a text box will be used for input. For any values that a user need to specify at the time that the stage is being changed, your application will need to determine how to gather that input. In these cases other type values could be used to present a different input field to the user. For example, maybe you'd want to use a :date format to indicate to your application to show a date picker.
Below is an exampe of how an action's implementation may define an options hash. This could either be hardcoded into the implementation file, or could be dynamically generated. This hash would specify 3 parameters:
-
:recipients would be a required text parameter
-
:priority would be an optional choice parameter with three choices (High, Medium or Low)
-
:delayed_send would be a required boolean parameter
OPTIONS = { :recipients => { :required=>true, :type=>:text, :description=>"The recipients of the notification" }, :priority => { :required=>false, :type=>:choice, :choices=>[{id: 1, value: "High"}, {id: 2, value: "Medium"},{id:3, value: "Low"} ] :description=>"The priority of the notification" }, :delayed_send => { :required=>true, :type=>:boolean, :default=>false, :description=>"Should we delay sending the notification" } }
This section will discuss extending workflowable.
Actions are code modules that can be run when moving items between Workflow stages. Actions can be developed to do just about anything--add a comment to a ticket, make API calls to an external system, send out notification emails, etc. There are three types of transition actions:
- Global actions--Run during every transition
- Before actions--Run before moving into a specific stage
- After actions--Run when moving out of a specific stage
All actions are developed the same way: extending Workflowable::Actions::Action and placing the Ruby file in the lib/workflowable/actions folder.
For the purpose of this walkthrough, we will add three actions that we can select and use in the interface (even though they don't actually do anything!)
Create three files:
lib/workflowable/actions/comment_action.rb
lib/workflowable/actions/notification_action.rb
lib/workflowable/actions/assignee_action.rb
For the comment_action.rb file use the following code:
class Workflowable::Actions::CommentAction < Workflowable::Actions::Action
# This is a name for the action and is how you will identify the action in the web application
NAME="Comment Action"
# This is a hash of options that can/must be passed moving between stages that will run this action
# The keys are used to identify the parameters
# The value for each key specifies options including a description that can be accessed and shown in
# the web interface, the type of parameter (this can be used to customize forms shown to the user), and # whether the parameter is required in order to transitions to the next stage.
OPTIONS = {
:message => {
:required=>true,
:type=>:text,
:description=>"The contents of the message"
}
}
# This function will be run when transitions between stages. This function doesn't do anything
# except print "Commenting!", but it could do other things!
def run
puts "Commenting!"
end
end
For the notification_action.rb file use the following code:
class Workflowable::Actions::NotificationAction < Workflowable::Actions::Action
NAME="Notification Action"
OPTIONS = {
:message => {
:required=>true,
:type=>:text,
:description=>"The contents of the notification"
}
}
def run
puts "Notifying!"
end
end
For the assignee_action.rb file use the following code:
class Workflowable::Actions::AssigneeAction < Workflowable::Actions::Action
NAME="Assignee Action"
OPTIONS = {
:message => {
:required=>true,
:type=>:text,
:description=>"Name of the assignee"
}
}
def run
puts "Assigning!"
end
end
Don't forget to restart the application!
Once these files are created, we'll be able to use them in our Workflows. We'll walkthrough the steps to do this in the Advanced: Add Actions section below.
- Discussion parameter for set_stage, next_step_options, etc. (mainly user)
- Discuss all objects that can be used/get passed to actions (@object, etc.)