Skip to content
This repository was archived by the owner on Aug 31, 2018. It is now read-only.
Andy Hoernecke edited this page Aug 19, 2014 · 3 revisions

Workflowable

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.

Installation and Setup

This section will walk through the steps for installing and setting up workflowable.

Installation

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

Admin Interface

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

Authorization

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 

Adding Workflow to a Model

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

Workflowable Concepts

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)

Creating and Configuring a Workflow

This section will walkthrough the steps needed to create a basic workflow.

Create a New Workflow

  1. Go to the workflowable admin section (/workflowable)
  2. Click "New Workflow"
  3. Give your workflow a name

Adding Stages

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).

  1. Click "Add a Stage"
  2. Enter "New" for the name of the stage
  3. Repeat for each Stage clicking "Add a Stage" and entering the stage's name.
  4. When all stages have been added, click "Create Workflow"

Configure Transitions

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.

  1. 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.

  1. Under "New" click "Add a next step"
  2. In the dropdown select "Investigating"
  3. Under "Investigating" click "Add a next step"
  4. In the dropdown select "Complete"
  5. Again under "Investigating" click "Add a next step"
  6. In the dropdown select "Not an Issue"
  7. Under "Not an Issue" click "Add a next step"
  8. In the dropdown select "Investigating"
  9. 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.

Advanced: Add Actions

You can optionally add actions to be run when transitions between stages.

So for example, imagine we're we want the following behavior:

  1. 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.
  2. When an item is moved into the "Investigating" stage, we want to choose an assignee. This will be a Before Action.
  3. 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.

  1. From the workflow list page (/workflowable) click edit next to our workflow.
  2. To create the global workflow, under Global Actions click "Add global action"
  3. Give the action the name "Notification Action"
  4. In the position field input 1. This specifies the order that the global actions will be run (were there more than 1!)
  5. 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.

  1. 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.

  1. Under the Investgating stage, click "Add before action" under Before Actions
  2. Give the action the name "Assignee Action"
  3. In the position field input 1
  4. In the action dropdown select "Assignee Action"

For this action's parameter, let's set a default value:

  1. 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.

  1. Under the Investgating stage, click "Add after action" under After Actions
  2. Give the action the name "Comment Action"
  3. In the position field input 1
  4. 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.

Using a Workflow

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.

  1. Open up the rails console

     rails c
    
  2. 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.

  1. 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 ... >
    
  2. 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!

Workflowable API

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.

acts_as_workflowable Model Methods

This section will disucss the methods inherited by models that include "acts_as_workflowable"

stage

This will return the Workflowable::Stage object for the object's current stage.

Parameters

None

Return Value

The object's current stage (Workflowable::Stage)

Example
ticket.stage # Assume ticket is an instance of an acts_as_workflowable model
# #<Workflowable::Stage id: 2, name: "Investigating", workflow_id: 1,

stage.next_steps

By retrieving the object's current stage we can then call next_steps to determine which stages the object could move into next.

Parameters

None

Return Value

An array with possible next stages (ArrayWorkflowable::Stage)

Example
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" ...>, ...]

next_step_options

Returns an options hash to indicate which options are available and/or required to change to the indicated stage.

Parameters

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

Return Value

The object's current stage (Workflowable::Stage)

Example
ticket.next_step_options(3, {}, current_user) # Assume ticket is an instance of an acts_as_workflowable model
# { :global=>{}, :before=>{}, :after=>{}} # No options available

validate_actions

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.

Parameters

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

Return Value

Array of error messages from the actions.

key: Name of the action with invalid parameters

value: The message provided by the action

Example
ticket.validate_actions(3, {}, current_user) # Assume ticket is an instance of an acts_as_workflowable model
# [{"Notification Action"=>["message is required\n"]}]

set_stage

Move the object into the specified stage.

Parameters

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

Return Value

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

Example
# 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

acts_as_workflowable Action Options Format

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.

Defining Options

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.

Example

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"
      	}
        }
    

Passing Options

Extending Workflowable

This section will discuss extending workflowable.

Advanced: Develop and Add Actions

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.

Action API

Todo

  • Discussion parameter for set_stage, next_step_options, etc. (mainly user)
  • Discuss all objects that can be used/get passed to actions (@object, etc.)
Clone this wiki locally