NAME

Workflow - Simple, flexible system to implement workflows

SYNOPSIS

use Workflow::Factory qw( FACTORY );

# Defines a workflow of type 'myworkflow'
my $workflow_conf  = 'workflow.xml';

# contents of 'workflow.xml'
<workflow>
    <type>myworkflow</type>
    <state name="INITIAL">
        <action name="upload file" resulting_state="uploaded" />
    </state>
    <state name="uploaded" autorun="yes">
        <action name="verify file" resulting_state="annotate">
             <!-- everyone other than 'CWINTERS' must verify -->
             <condition test="$context->{user} ne 'CWINTERS'" />
        </action>
        <action name="null" resulting_state="annotated">
             <condition test="$context->{user} eq 'CWINTERS'" />
        </action>
    </state>
    <state name="verify file">
        <action name="annotate">
            <condition name="can_annotate" />
        </action>
    </state>
    <state name="annotated">
        <action name="null" resulting_state="finished" />
    </state>
    <state name="finished" />
</workflow>

# Defines actions available to the workflow
my $action_conf    = 'action.xml';

# contents of 'action.xml'
<actions>

    <action name="upload file" class="MyApp::Action::Upload">
        <field name="path" label="File Path"
               description="Path to file" is_required="yes" />
    </action>

    <action name="verify file" class="MyApp::Action::Verify">
        <validator name="filesize_cap">
            <arg>$file_size</arg>
        </validator>
    </action>

    <action name="annotate"    class="MyApp::Action::Annotate" />

    <action name="null"        class="Workflow::Action::Null" />

</action>

# Defines conditions available to the workflow
my $condition_conf = 'condition.xml';

# contents of 'condition.xml'
<conditions>
    <condition name="can_annotate"
               class="MyApp::Condition::CanAnnotate" />
</conditions>

# Defines validators available to the actions
my $validator_conf = 'validator.xml';

# contents of 'validator.xml'
<validators>
    <validator name="filesize_cap" class="MyApp::Validator::FileSizeCap">
        <param name="max_size" value="20M" />
    </validator>
</validators>

# Stock the factory with the configurations; we can add more later if
# we want
FACTORY->add_config_from_file(
    workflow   => $workflow_conf,
    action     => $action_conf,
    condition  => $condition_conf,
    validator  => $validator_conf
);

# Instantiate a new workflow...
my $workflow = FACTORY->create_workflow( 'myworkflow' );
print "Workflow ", $workflow->id, " ",
      "currently at state ", $workflow->state, "\n";

# Display available actions...
print "Available actions: ", $workflow->get_current_actions, "\n";

# Get the data needed for action 'upload file' (assumed to be
# available in the current state) and display the fieldname and
# description
 
print "Action 'upload file' requires the following fields:\n";
foreach my $field ( $workflow->get_action_fields( 'FOO' ) ) {
    print $field->name, ": ", $field->description,
          "(Required? ", $field->is_required, ")\n";
}
 
# Add data to the workflow context for the validators, conditions and
# actions to work with

my $context = $workflow->context;
$context->param( current_user => $user );
$context->param( sections => \@sections );
$context->param( path => $path_to_file );

# Execute one of them
$workflow->execute_action( 'upload file' );

print "New state: ", $workflow->state, "\n";

# Later.... fetch an existing workflow
my $id = get_workflow_id_from_user( ... );
my $workflow = FACTORY->fetch_workflow( 'myworkflow', $id );
print "Current state: ", $workflow->state, "\n";

DESCRIPTION

This documentation is for version '0.10' of the Workflow module.

Overview

This is a standalone workflow system. It is designed to fit into your system rather than force your system to fit to it. You can save workflow information to a database or the filesystem (or a custom storage). The different components of a workflow system can be included separately as libraries to allow for maximum reusibility.

User Point of View

As a user you only see two components, plus a third which is really embedded into another:

  • Workflow::Factory - The factory is your interface for creating new workflows and fetching existing ones. You also feed all the necessary configuration files and/or data structures to the factory to initialize it.

  • Workflow - When you get the workflow object from the workflow factory you can only use it in a few ways -- asking for the current state, actions available for the state, data required for a particular action, and most importantly, executing a particular action. Executing an action is how you change from one state to another.

  • Workflow::Context - This is a blackboard for data from your application to the workflow system and back again. Each instantiation of a Workflow has its own context, and actions executed by the workflow can read data from and deposit data into the context.

Developer Point of View

The workflow system has four basic components:

  • workflow - The workflow is a collection of states; you define the states, how to move from one state to another, and under what conditions you can change states.

    This is represented by the Workflow object. You normally do not need to subclass this object for customization.

  • action - The action is defined by you or in a separate library. The action is triggered by moving from one state to another and has access to the workflow and more importantly its context.

    The base class for actions is the Workflow::Action class.

  • condition - Within the workflow you can attach one or more conditions to an action. These ensure that actions only get executed when certain conditions are met. Conditions are completely arbitrary: typically they will ensure the user has particular access rights, but you can also specify that an action can only be executed at certain times of the day, or from certain IP addresses, and so forth. Each condition is created once at startup then passed a context to check every time an action is checked to see if it can be executed.

    The base class for conditions is the Workflow::Condition class.

  • validator - An action can specify one or more validators to ensure that the data available to the action is correct. The data to check can be as simple or complicated as you like. Each validator is created once then passed a context and data to check every time an action is executed.

    The base class for validators is the Workflow::Validator class.

WORKFLOW BASICS

Just a Bunch of States

A workflow is just a bunch of states with rules on how to move between them. These are known as transitions and are triggered by some sort of event. A state is just a description of object properties. You can describe a surprisingly large number of processes as a series of states and actions to move between them. The application shipped with this distribution uses a fairly common application to illustrate: the trouble ticket.

When you create a workflow you have one action available to you: create a new ticket ('create issue'). The workflow has a state 'INITIAL' when it is first created, but this is just a bootstrapping exercise since the workflow must always be in some state.

The workflow action 'create issue' has a property 'resulting_state', which just means: if you execute me properly the workflow will be in the new state 'CREATED'.

All this talk of 'states' and 'transitions' can be confusing, but just match them to what happens in real life -- you move from one action to another and at each step ask: what happens next?

You create a trouble ticket: what happens next? Anyone can add comments to it and attach files to it while administrators can edit it and developers can start working on it. Adding comments does not really change what the ticket is, it just adds information. Attachments are the same, as is the admin editing the ticket.

But when someone starts work on the ticket, that is a different matter. When someone starts work they change the answer to: what happens next? Whenever the answer to that question changes, that means the workflow has changed state.

Discover Information from the Workflow

In addition to declaring what the resulting state will be from an action the action also has a number of 'field' properties that describe that data it required to properly execute it.

This is an example of discoverability. This workflow system is setup so you can ask it what you can do next as well as what is required to move on. So to use our ticket example we can do this, creating the workflow and asking it what actions we can execute right now:

my $wf = Workflow::Factory->create_workflow( 'Ticket' );
my @actions = $wf->get_current_actions;

We can also interrogate the workflow about what fields are necessary to execute a particular action:

print "To execute the action 'create issue' you must provide:\n\n";
my @fields = $wf->get_action_fields( 'create issue' );
foreach my $field ( @fields ) {
    print $field->name, " (Required? ", $field->is_required, ")\n",
          $field->description, "\n\n";
}

Provide Information to the Workflow

To allow the workflow to run into multiple environments we must have a common way to move data between your application, the workflow and the code that moves it from one state to another.

Whenever the Workflow::Factory creates a new workflow it associates the workflow with a Workflow::Context object. The context is what moves the data from your application to the workflow and the workflow actions.

For instance, the workflow has no idea what the 'current user' is. Not only is it unaware from an application standpoint but it does not presume to know where to get this information. So you need to tell it, and you do so through the context.

The fact that the workflow system proscribes very little means it can be used in lots of different applications and interfaces. If a system is too closely tied to an interface (like the web) then you have to create some potentially ugly hacks to create a more convenient avenue for input to your system (such as an e-mail approving a document).

The Workflow::Context object is extremely simple to use -- you ask a workflow for its context and just get/set parameters on it:

# Get the username from the Apache object
my $username = $r->connection->user;

# ...set it in the context
$wf->context->param( user => $username );

# somewhere else you'll need the username:

$news_object->{created_by} = $wf->context->param( 'user' );

Controlling What Gets Executed

A typical process for executing an action is:

  • Get data from the user

  • Fetch a workflow

  • Set the data from the user to the workflow context

  • Execute an action on the context

When you execute the action a number of checks occur. The action needs to ensure:

  • The data presented to it are valid -- date formats, etc. This is done with a validator, more at Workflow::Validator

  • The environment meets certain conditions -- user is an administrator, etc. This is done with a condition, more at Workflow::Condition

Once the action passes these checks and successfully executes we update the permanent workflow storage with the new state, as long as the application has declared it.

WORKFLOW METHODS

The following documentation is for the workflow object itself rather than the entire system.

Object Methods

execute_action( $action_name )

Execute the action $action_name. Typically this changes the state of the workflow. If $action_name is not in the current state, fails one of the conditions on the action, or fails one of the validators on the action an exception is thrown.

Returns: new state of workflow

get_current_actions()

Returns a list of action names available from the current state for the given environment. So if you keep your context() the same if you call execute_action() with one of the action names you should not trigger any condition error since the action has already been screened for conditions.

Returns: list of strings representing available actions

get_action_fields( $action_name )

Return a list of Workflow::Action::InputField objects for the given $action_name. If $action_name not in the current state or not accessible by the environment an exception is thrown.

Returns: list of Workflow::Action::InputField objects

add_history( \%params | $wf_history_object )

Adds history to the workflow, typically done by an action in execute_action() or one of the observers of that action. This history will not be saved until execute_action() is complete.

Returns: nothing

get_history()

Returns list of history objects for this workflow. Note that some may be unsaved if you call this during the execute_action() process.

get_unsaved_history()

Returns list of all unsaved history objects for this workflow.

clear_history()

Clears all transient history objects from the workflow object, not from the long-term storage.

Properties

Unless otherwise noted properties are read-only.

id

ID of this workflow. This will always be defined, since when the Workflow::Factory creates a new workflow it first saves it to long-term storage.

type

Type of workflow this is. You may have many individual workflows associated with a type.

description

Description (usually brief, hopefully with a URL...) of this workflow.

state

The current state of the workflow.

context (read-write, see below)

A Workflow::Context object associated with this workflow. This should never be undefined as the Workflow::Factory sets an empty context into the workflow when it is instantiated.

If you add a context to a workflow and one already exists, the values from the new workflow will overwrite values in the existing workflow. This is a shallow merge, so with the following:

$wf->context->param( drinks => [ 'coke', 'pepsi' ] );
my $context = Workflow::Context->new();
$context->param( drinks => [ 'beer', 'wine' ] );
$wf->context( $context );
print 'Current drinks: ', join( ', ', @{ $wf->context->param( 'drinks' ) } );

You will see:

Current drinks: beer, wine

Internal Methods

init( $id, $current_state, \%workflow_config, \@wf_states )

THIS SHOULD ONLY BE CALLED BY THE Workflow::Factory. Do not call this or the new() method yourself -- you will only get an exception. Your only interface for creating and fetching workflows is through the factory.

This is called by the inherited constructor and sets the $current_state value to the property state and uses the other non-state values from \%config to set parameters via the inherited param().

_get_action( $action_name )

Retrieves the action object associated with $action_name in the current workflow state. This will throw an exception if:

  • No workflow state exists with a name of the current state. (This is usually some sort of configuration error and should be caught at initialization time, so it should not happen.)

  • No action $action_name exists in the current state.

  • No action $action_name exists in the workflow universe.

  • One of the conditions for the action in this state is not met.

_get_workflow_state( [ $state ] )

Return the Workflow::State object corresponding to $state, which defaults to the current state.

_set_workflow_state( $wf_state )

Assign the Workflow::State object $wf_state to the workflow.

_get_next_state( $action_name )

Returns the name of the next state given the action $action_name. Throws an exception if $action_name not contained in the current state.

SEE ALSO

Workflow::Context

Workflow::Factory

Workflow::State

COPYRIGHT

Copyright (c) 2003 Chris Winters and Arvato Direct; 2004 Chris Winters. All rights reserved.

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

AUTHORS

Chris Winters <chris@cwinters.com>

Dietmar Hanisch <Dietmar.Hanisch@Bertelsmann.de> - Provided most of the good ideas for the module and an excellent example of everyday use.

Jim Smith <jgsmith@tamu.edu> - Contributed patches and ideas.

Martin Winkler <mw@arsnavigandi.de> - Pointed out a bug and a few other items.