Package Developer's Guide to Workflow

Workflow Documentation : Package Developer's Guide

By Lars Pind

Introduction

The workflow package manages a collaborative process surrounding some object.

Examples of the object could be a bug in a bug tracker, a ticket in a ticket tracker, a content item in a content management system, a user contribution in a moderated forum/comments/whatever application, a user's request for particpation in a course/event/whatever.

For example, when a new bug is submitted, someone's assigned to fix it, and whoever submitted it is assigned to verify the fix and close the bug. Once the bug's fixed, the submitter will get notified, and the bug will wait in the 'resolved' state until the submitter has verified and then closed the bug.

In order to make use of workflow in your own application, here are the things you need to consider:

  1. Define your default process. The idea typically is to allow your end users to modify the process to suit their needs, but you'll want eto provide a process which they can use as a starting point.
  2. Identify, declare, and implement the callbacks that your application will need.
  3. Write the code to set up the initial process, and to clone that process for each package instance.
  4. Integrate workflow support into your application's API.
  5. Integrate workflow support into your application's user interface.
  6. Integrate workflow into your application's queries

Let's first look at some concepts before getting into the technicalities of how you actually do this. For a working example of building workflow-based applications, we recommend you take a look at bug-tracker.

Workflow Concepts

What's in a workflow

In its broadest, most conceptual sense, a workflow is defined as (and this does get a little hairy, so feel free to skip if you just want to start developing your applicaton):

Notation:

[Action] (State) {Role}

(State1) -> [Action1] -> (State2) -> [Action2] -> (State3)

Cases

So much for defining the workflow. When you start "running" your workflow, that's called a workflow case, or simply a case. A case is concerned with a particular object, it's always in a particular state, and it has specific people or groups assigned to its different roles.

In-flow and out-of-flow

When defining actions, we differentiate between in-flow and out-of-flow. In-flow refers to the normal idealized flow of the workflow, out-of-flow are the rest. Concretely what it means is that if you're assigned to an in-flow action, we'll bug you about it through notifications, and potentially get mad at you if you don't come and do something to get the workflow moving along. We don't do that with out-of-flow actions. So we'll send a notification that says "Please come back and resolve this bug", whereas we'll not notify everybody who are allowed to comment saying "Please come back and comment on this bug".

For bug-tracker, the normal flow (in-flow) is this:

(Open) -> [Resolve] -> (Resolved) -> [Close] -> (Closed)

Other actions not in the normal flow are [Edit] and [Comment], which are always enabled, but never change the state. And [Reopen] which throw you back to the (Open) state. And finally [Resolved] is in-flow when in the (Open) state, but out-of-flow when in the (Resolved) state, meaning that you can re-resolve a bug if you need to, but you're not required to.

In-flow and out-of-flow depends on the action, the state, and the user's role in the case. For example, it might be that users in the {Submitter} role are allowed to resolve their own bugs, but the [Resolve] action will still only be considered in-flow to people in the {Assignee} or {Resolver} role.

The Six Steps Conceptually

The recommended way a workflow is linked to an application is this: As part of developing your application, you define your default workflow, which will be used as a template for customization by the users of your applications. This workflow will be installed using the APM after-install callback, and will be linked to your application's package-key.

Then when a new instance of your application is created, your default workflow will be cloned, and the clone linked to the new instance, so that your users can customize the workflow for each instance of your application individually. The default copy installed along with your package is never actually used except for cloning when creating a new instance. This means that your users can customize this deafult workflow, and the modified version will serve as the boilerplate for all new package instances.

In order to integrate workflow with your application, you'll want to implement one or more of the callback service contracts. These can do things like determine default assignees based on certain data in your application, get information about your application's object for use when sending out notifications, or perform side-effects, such as actually changing the publication state of a content item when you execute the [Publish] action.

When integrating the workflow with your application's user experience, what workflow will give you is essentially the list of actions that the given user can perform on the given object at the given time. In bug-tracker, for example, bug-tracker takes care of displaying the form displaying and editing a bug, while workflow takes care of displaying the buttons that say [Comment], [Edit], [Resolve], [Reopen], [Close], etc., along the bottom of the form. Workflow also has a place to store which form elements should be opened for editing depending on the action being executed.

Your application should typically have an API for creating a new object, editing an object, etc. This application object API will need to be workflow-aware, so when a new object is created, a new workflow case will be started as well. And when the object's edited, that should generally only happen through a workflow action, so that needs to be taken into account as well.

The final step is integrating workflow into your application's queries when you want to filter an object listing/count based on the workflow state. This is the only place where you'll directly be dependent on the workflow data model.

Defining Your Process (FSM)

The current version of workflow only supports workflows based on a finite state machine (FSM). Support for other models, such as petri nets and directed graphs are possible, but not currently designed or implemented.

An FSM-based workflow consists of a set of states, actions, and roles.

You define a new workflow like this:

set spec {
    workflow-short-name {
        ...
        roles {
            role-short-name {
               ...
            }
            ...
        }
        states {
            state-short-name {
               ...
            }
            ...
        }
        actions {
            action-short-name {
               ...
            }
            ...
        }
    }
}

set workflow_id [workflow::fsm::new_from_spec -spec $spec]

All the items (workflow, roles, states, actions) have a short-name, which should be lowercase and use underbar (_) instead of spaces. These are mainly used to refer to the items in other parts of the spec.

The workflow short name can be used to refer to the workflow in your application, which is useful if you have several different workflows. The bug-tracker, for example, could have a workflow for bugs and another one for patches.

Finally, you can also refer states, roles, and actions in your application using short names, although this creates a dependency in your application on a particular form of the workflow, and there's currently no mechanism for ensuring that your workflow contains the states, roles, and actions you'd refer to. This is on the todo-list.

Workflow

These are the attributes you can specify for the workflow itself:

Attribute Description
pretty_name Name used in the user interface.
package_key The package that defined this workflow.
object_type The parent object type which this workflow can be applied to. If your workflow applies to any object, say 'acs_object'. This is used in relation to callbacks when we build the user interface for defining workflows. More on this in the section on callbacks.
callbacks Callbacks that apply to the whole workflow. If you add side-effect callbacks, these are executed every time any action is executed.
roles Denotes the section of the spec that defines the workflow's roles.
states Denotes the section of the spec that defines the workflow's states.
actions Denotes the section of the spec that defines the workflow's actions.
Internationalization Note:

When we make workflow internationalized for OpenACS 5.0, pretty names will contain message keys in the form "#message-key#". More about this in the package developer's guide to internationalization.

Roles

Attributes for roles:

Attribute Description
pretty_name Name used in the user interface.
callbacks Callbacks that define how assignment of this role to users is done.

States

A few typical examples of states:

Application States
Ticket Tracker (Open),(Completed), and (Closed)
Bug Tracker (Open), (Triaged), (Resolved), and (Closed)
Content Management System Publication (Authored), (Edited), and (Published)
Simple Approval (Requested), (Approved), and (Rejected)

These are the state attributes in the workflow specification:

Attribute Description
pretty_name Name used in the user interface.
hide_fields A tcl list of form elements/object attributes that don't make sense in this state. In bug-tracker, the element "Fixed in version" doesn't make sense when the bug is (Open), and thus not yet fixed. It's currently up to your application to do incorporate this into your application.

Actions

Actions are what the workflow allows you to do to your object.

Terminology:
Enabled
The action is allowed to be executed in the workflow's current state.
Allowed
The given user is allowed to execute the action given his current relationship to the workflow case and the object.
Assigned
The same as allowed, but the action is in-flow for this user.
Available
The action is both enabled and allowed for this user.

Some actions will always be enabled. In bug-tracker, for example, we have [Comment] and [Edit] actions, which are always allowed, regardless of whether the bug is (Open), (Resolved), or (Closed).

Other actions, however, will only be enabled in certain states. In bug-tracker, for example, the [Close] action is only available in the (Resolved) state.

Another distinction is that some actions change the state, and others do not. [Comment] and [Edit], for example, do not. [Resolve], [Close], and [Reopen] do. For an FSM, when an action changes the state, you simply specify what the new state should be.

There's a special action called the initial action. This is implicitly executed when a new case is started for this workflow, and must always specify the "new_state" attribute to define which state new cases start out in.

Attributes for actions:

Attribute Description
pretty_name Name used in the user interface.
pretty_past_tense This is used in the case log to say "<pretty_past_teense> by <user> on <date>", for example "Resolved by Jeff Davis on April 15, 2003".
new_state The short_name of the state this action puts the case into. Leave out if the action shouldn't change the state.
initial_action_p Say 't' if this is the initial action. Leave out or set to 'f' otherwise.
allowed_roles A list of roles that are allowed but not assigned to perform this action.
assigned_role A single role which is assigned to this action.
privileges A list of privileges. Users who have been granted one of these privileges on the case's object will be allowed to execute this action.
always_enabled_p Say 't' if this action should be enabled regardless of the case's current state. Say 'f' or leave out otherwise.
enabled_states If not always enabled, enumerate the states in which this action is enabled but not assigned.
assigned_states Enumerate the states in which this action is enabled and assigned.
edit_fields A tcl list of fields which should be opened for editing when the user is performing this action. Again, it's up to the application to act on this.
callbacks Side-effect callbacks which are executed when this action is executed.

Putting A Workflow Together

When you put this all together, here's a real live example of what defining a workflow could look like:

ad_proc -private bug_tracker::bug::workflow_create {} {
    Create the 'bug' workflow for bug-tracker
} {
    set spec {
        bug {
            pretty_name "Bug"
            package_key "bug-tracker"
            object_type "bt_bug"
            callbacks { 
                bug-tracker.FormatLogTitle 
                bug-tracker.BugNotificationInfo
            }
            roles {
                submitter {
                    pretty_name "Submitter"
                    callbacks { 
                        workflow.Role_DefaultAssignees_CreationUser
                    }
                }
                assignee {
                    pretty_name "Assignee"
                    callbacks {
                        bug-tracker.ComponentMaintainer
                        bug-tracker.ProjectMaintainer
                        workflow.Role_PickList_CurrentAssignees
                        workflow.Role_AssigneeSubquery_RegisteredUsers
                    }
                }
            }
            states {
                open {
                    pretty_name "Open"
                    hide_fields { resolution fixed_in_version }
                }
                resolved {
                    pretty_name "Resolved"
                }
                closed {
                    pretty_name "Closed"
                }
            }
            actions {
                open {
                    pretty_name "Open"
                    pretty_past_tense "Opened"
                    new_state "open"
                    initial_action_p t
                }
                comment {
                    pretty_name "Comment"
                    pretty_past_tense "Commented"
                    allowed_roles { submitter assignee }
                    privileges { read write }
                    always_enabled_p t
                }
                edit {
                    pretty_name "Edit"
                    pretty_past_tense "Edited"
                    allowed_roles { submitter assignee }
                    privileges { write }
                    always_enabled_p t
                    edit_fields { 
                        component_id 
                        summary 
                        found_in_version
                        role_assignee
                        fix_for_version
                        resolution 
                        fixed_in_version 
                    }
                }
                reassign {
                    pretty_name "Reassign"
                    pretty_past_tense "Reassigned"
                    allowed_role { submitter assignee }
                    privileges { write }
                    enabled_states { resolved }
                    assigned_states { open }
                    edit_fields { role_assignee }
                }
                resolve {
                    pretty_name "Resolve"
                    pretty_past_tense "Resolved"
                    assigned_role "assignee"
                    enabled_states { resolved }
                    assigned_states { open }
                    new_state "resolved"
                    privileges { write }
                    edit_fields { resolution fixed_in_version }
                    callbacks { bug-tracker.CaptureResolutionCode }
                }
                close {
                    pretty_name "Close"
                    pretty_past_tense "Closed"
                    assigned_role "submitter"
                    assigned_states { resolved }
                    new_state "closed"
                    privileges { write }
                }
                reopen {
                    pretty_name "Reopen"
                    pretty_past_tense "Reopened"
                    allowed_roles { submitter }
                    enabled_states { resolved closed }
                    new_state "open"
                    privileges { write }
                }
            }
        }
    }

    set workflow_id [workflow::fsm::new_from_spec -spec $spec]
    
    return $workflow_id
}

Defining Callbacks

There are a number of different types of callbacks, each of which applies to different workflow items (workflows, roles, states, actions). They're all defined as service contracts.

In order to make use of them, your application will need to implement these service contracts, and register the implementation with the relevant workflow item through the 'callbacks' attribute in the spec above.

Here are the types of callbacks defined, how they're used, and the workflow items they apply to.

Service Contract Applies To Description
Workflow.Role_DefaultAssignees Roles Used to get the default assignees for a role. Called for all roles when a case is started. Also called for roles with no assignees, when that role is assigned to an action.
Workflow.Role_AssigneePickList Roles Used when the users wants to reassign a role to populate a drop-down list of the most likely users/groups to assign this role to. Should return less than 25 users/groups.
Workflow.Role_AssigneeSubQuery Roles A subquery used to limit the scope of the user's search for a new assignee for a role. Could typically be used to limit the search to members of a particular group, organizers of a particular event, etc.
Workflow.Action_SideEffect Workflows, Actions This is executed whenever the given action is executed. If specified for the workflow, it will be executed whenever any action is executed on the workflow.
Workflow.ActivityLog_FormatTitle Workflows Used to format the title of the case log. In bug-tracker, this is used to get the resolution code displayed in the case log as "Resolved (Fixed)" or "Resolved (Not Reproducable)".
Workflow.NotificationInfo Workflows Allows the application to supply information about the case object for the notification.

$$$ service contract operations, input, output

$$$ linking implementations to object types.

Installing and Instantiating

Integrating With Your Application's API

Integrating With Your Application's User Interface

Integrating With Your Application's Queries


lars@pinds.com