OSWf Concepts

OSWf is fairly different from most other workflow systems available, both commercially and in the open source world. What makes OSWf different is that it is application devloper oriented and hence extermely flexible. However, this can be hard to grasp at first. For example, OSWf does not have a graphical tool for developing workflows, and the recommended approach is to write the XML workflow descriptors 'by hand' with your favorite text or XML editor. It is up to the application developer to provide this sort of integration, as well as any integration with existing code and databases. These may seem like problems to someone who is looking for a quick "plug-and-play" workflow solution, but we've found that such a solution never provides enough flexibility to properly fulfill all requirements in a full-blown application.

OSWf gives the developer flexibility

OSWf can be considered a "low level" workflow implementation. Situations like "loops" and "conditions" that might be represented by a graphical icon in other workflow systems are "coded" in OSWorkflow. That's not to say that actual code is needed to implement situations like this, but a scripting language must be employed to specify these conditions. It is not expected that a non-technical user modify workflow. We've found that although some systems provide GUIs that allow for simple editing of workflows, the applications surrounding the workflow usually end up damaged when changes like these are made. We believe it is best for these changes to be made by a developer who is aware of each change.

OSWf is based heavily on the concept of the finite state machine. Each state in represented by the combination of a step ID and a status. A transition from one state to another cannot happen without an action occuring. There are always at least one or more active states during the lifetime of a workflow. These simple concepts are what lie at the core of the OSWf process engine and allow a simple XML file to be translated in to business workflow processes.

Workflow Descriptor

At the heart of OSWf is the workflow definition descriptor, also know as the process descriptor. The descriptor is an XML file.

This descriptor describes all the steps, states, transitions, and functionality for a given workflow.

OSWf is very unique compared to other workflow engines one might be familiar with. In order to completely grasp OSWf and properly harness the features available, it is important that one understand the core concepts that form the foundation for OSWf.

Steps, Status, andActions

Any particular process instance can have one or more current steps at any given moment. Every current step has a status associated to it. The statuses of all of the current steps constitute workflow status for that process instance. The actual statuses are entirely up to the application developer and/or project manager. A status is string and can be, for example, "Underway" "Ready" or "Pending".

For the workflow to progress, a transition must take place in the finite state machine that represents a workflow instance. Once a step is completed it can not be current. Usually a new current step is created immediately thereafter, which keeps the workflow going. The final status of the completed step is set by the exit-status attribute. It happens just before the transition to another step. exit-status must already be defined when a new transition takes place in the workflow. It can be any value you please, but "Finished" or "Completed" usually works for most applications.

Transition itself is a result of an action. A step may have many actions connected to it. Which particular action will be launched is determined by the end user, external event or automatically by a trigger. Depending on the action accomplished, a certain transition takes place. Actions can be restricted to particular groups and users or current state of the workflow. Each action must have one unconditional result (default) and zero or more conditional results.

So, to summarize, a workflow consists of a number of Steps. A step has a current status (for example, Queued, Underway, or Finished). A step has a number of Actions that can be performed in it. An action has Conditions under which it is available, as well as Functions that are executed. Actions have results that change the state and current step of the workflow.

Results, Joins, and Splits

Default Result

For every action, it is required that there exist at least one result, called the default-result. A result is nothing more than a series of directives that tell the process engine what the next step is. This involves making a transition from one step to the next step(s) in the state machine that makes up a given workflow.

In OSWf the tag default-result replaced the tag unconditional-result from OSWorkflow. The tag unconditional-result remains supported but has been deprecated.

Conditional Results

A conditional-result is similiar to default-result except for the fact that it requires one or more additional conditions. The first conditional-result that evaluates to true (using the types AND or OR) will dictate which transition that takes place. Additional information regarding conditions can be found below.

There are three different results (conditional or default) that can occur:

One caveat regarding transitions, currently a split or a join cannot result in an immediate split or join again. However, this is not a major issue, simply insert a step with a single automatic action between the splits and joins.

A single step result can be specified simply by taking all of the default attributes, for example:

  <default-result step='2' />

Alternatively, the optional attributes can be specified as in this exampe:

  <default-result exit-status='Finished' status='Pending' step='2' owner='${actor}' />

In certain cases the result of an action does not require a transition to another step i.e. this step is the end of the workflow. Such a result may be specified by setting the step value to -1. For example, we can change the above example to remain in the current step (or steps) as follows:

  <default-result status='Repeat' step='-1' />

Splitting from one step to multiple steps can be achieved as shown below where the workflow state after the split consists of steps 200 & 300.

    <!-- From Step id=150 -->
    <default-result split='1'/>
            .
            .
            .
    <splits>
    
      <split id='1'>;
        <default-result step='200'  owner='${someActor}' />
        <default-result step='300'  owner='${someOtherActor}' />
      </split>
      
    </splits>

Joins are the most complex element but also the most interesting and powerful. A typical join might look like:

    <!-- From Step id=100 -->
      <default-result exit-status='Finished' join='1' />
            .
            .
            .
    <!-- From Step id=200  -->
      <default-result exit-status='Finished' join='1' />
            .
            .
            .
  <joins>

    <join id="1">
      <conditions type="AND">
        <condition type="beanshell">
          <arg name="script">
            "Finished".equals(jn.getStep(200).getStatus())  &&
            "Finished".equals(jn.getStep(300).getStatus())
          </arg>
        </condition>
      </conditions>
      <default-result step="400" />
    </join>
  
  </joins>

The above might seem somewhat cryptic, but the main thing to notice is that the condition XML element uses a special variable "jn" that can be used to make up expressions that determine when the join actually occurs. Essentially, this expression statement says "proceed to step 400 only when the steps with IDs 200 and 300 have transitioned into this join have a status of 'Finished'".

Being able to write code in order to determine how a join should proceed is on the greatest strenghts of OSWf. Besides using the jn variable to look at the status of incoming steps one can also use the name/value pairs stored in the PropertSet to create very flexible join logic.

Functions

OSWorkflow defines a standard way for external business logic and services to be defined and executed. This is accomplished by using "functions". A function usually encapsulates functionality that is external to the workflow instance itself, perhaps related to updating an external entity or system with workflow information, or notifying an external system regarding a change in workflow status.

There are two types of functions: pre and post step functions.

Pre functions are functions that are executed before the workflow makes a transition. An example is a pre function that sets up the name of the caller to use as the result for the state change that is about to take place. Another example of a pre-function is a function that updates the most recent caller of an action. Both of these are provided as standard utility functions that are very useful for practical workflows.

Post functions have the same range of applicability as pre functions, except that they are executed after the state change has taken place. An example of a post function is one that sends out an email to interested parties that the workflow has had a particular action performed on it. For example, when a document in the 'research' step has a 'markReadyForReview' action taken, the reviewers group is emailed.

There are many reasons for including pre and post functions. One is that if the user were to click the "done" button twice and to send out two "execute action" calls, and that action had a pre function that took a long time to finish, then it is possible the long function could get called multiple times, because the transition hasn't been made yet, and OSWorkflow thinks the second call to perform the action is valid. So changing that function to be a post function is what has to happen. Generally pre functions are for simple, quick executions, and post functions are where the bulk of the functional is defined.

Functions can be specified in two separate locations; steps and actions.

Usually, a pre or post function is specified in an action. The general case is that along with transitioning the workflow, a functions is used to 'do something', whether it be notifying a third party, sending an email, or simply setting variables for future use. The following diagram will help illustrate action level functions:

In the case of pre and post functions being specified on steps, the usage is slightly different. Pre-functions specified on a step will be executed before the workflow is transitioned to that step. Note that these functions will be applied indiscriminantly to ALL transitions to the step, even those that originate in the step itself (for example, moving from Queued to Underway within the same step will cause the invocation of any step pre-functions specified).

Similarly, step post-functions will be called prior to the workflow transitioning out of the step, even if it's to change state and remain within the step.

The following diagram illustrates the invocation order. Note that the action box is abbreviated and could well contain pre and post functions of its own.

Registers

A register is a helper function that returns an object that can be used in Functions for easy access to common objects, especially entities that revolve around the workflow. The object being registered can be any kind of object. Typical examples of objects being registered are: Document, Metadata, Issue, and Task. This is strictly for convenience and does not add any extra benefit to OSWorkflow besides making the developer's life much simpler. Here is an example of a register:

  <registers>
    <register name="doc" class="com.acme.DocumentRegister"/>
  </registers>
       .
       .
       .
    <results>
      <result condition="doc.priority == 1" step="1" status="Underway"  owner="${someManager}"/>
      <unconditional-result step="1" status="Queued"/>
    </results>

Conditions

Conditions, just like validators, registers, and functions, can be implemented in a variety of languages and technologies. Conditions can be grouped together using AND or OR logic. Any other kind of complex login must be implemented by the workflow developer. Conditions usually associated with conditional results, where a result is executed based on the conditions imposed on it being satisfied.

Conditions are very similar to functions except that they return boolean instead of void. You can find out more about Conditions.

Variable Interpolation

In all functions, conditions, validators, and registers it is possible to provide a set of args to the code of choice. These args are translated to the args Map that is discussed in further detail later on. Likewise the status, old-status, and owner elements in the workflow descriptor are also all parsed for variables to be dynamically converted. A variable is identified when it looks like ${foo}. OSWorkflow recognizes this form and first looks in the transientVars for the key foo. If the key does not exist as a transient variable, then then propertySet is searched. If the propertyset does not contain the specified key either, then the entire variable is converted to an empty String.

One thing of particular importance is that in the case of args, if the variable is the only argument, the argument will not be of type String, but instead whatever the variable type is. However, if the arg is a mix of characters and variables, the entire argument is converted to String no matter what. That means the two arguments below are very different in that foo is a Date object and bar is a String:

    <arg name="foo">${someDate}</arg>
    <arg name="bar"> ${someDate} </arg> <!-- note the extra spaces -->

Permissions and Restrictions

Permissions can be assigned to users and/or groups based on the state of the workflow instance. These permissions are unrelated to the functionality of the workflow engine, but they are useful to have for applications that implement OSWorkflow. For example, a document management system might have the permission name "file-write-permission" enabled for a particular group only during the "Document Edit" stage of the workflow. That way your application can use the API to determine if files can be modified or not. This is useful as there could be a number of states within the workflow where the "file-write-permission" is applicable, so instead of checking for specific steps or conditions, the check can simply be made for a particular permission.

Permissions and actions both use the concept of restrictions. A restriction is nothing more than one or more conditions embedded inside a restrict-to element.

Auto Actions

Sometimes it is desirable to have an action performed automatically, based on specific conditions. This is useful for example when trying to add automation to a workflow. In order to achieve this, an attribute of auto="true" will have to be added to the specific action. The workflow engine will then evaluate the conditions and restrictions on the action, and if they are matched and the workflow could perform the action, then it automatically does so. The auto action executes with the current caller, so the permissions checks and so on are performed against the user who called the action that initiated the auto action.

Process Variables - Name/Value Pairs

At any point in the workflow, you will likely want to persist small pieces of data. This is made possible in OSWorkflow by the use of the OpenSymphony PropertySet library. A PropertySet is basically a persistent type-safe Map. You can add items to the propertyset (one is created per workflow) and later on retrieve them. The propertyset is not emptied or deleted unless you explicitly do so yourself. Every function and condition has access to this propertyset, as well as any inline scripts, where it is added to the script context with the name 'propertySet'. So, to illustrate an inline script accessing the property set, let's add the following to our 'Start First Draft' actions' pre-functions.

  <function type=>"beanshell">
    <arg name="script">propertySet.setString("foo", "bar")</arg>
  </function>

We've now added a persistent property called 'foo', with the value of 'bar'. At any point in the workflow from now on, we will be able to retrieve the value assigned to 'foo' with the following peice of code.

  <function type=>"beanshell">
    <arg name="script">String value = propertySet.getString("foo")</arg>
  </function>

Transient variables

As well as the propertyset variable, the other important variable made available to workflow scripts, functions, and conditions is the 'transientVars' map. This map is simply a transient map that contains context specific information for the current workflow invocation. It includes the current workflow instance being manipulated, as well as the current workflow store and the workflow descriptor being used as well as other relevant values. You can see a list of all the available keys in the javadocs for FunctionProvider and Condition.

Inputs

Every invocation of a workflow action takes an optional input map. This map can contain any arbitrary data that you might want to make available to your functions or conditions. It is not persisted anywhere, and is simply a data-passing mechanism.

Registers

A register is a global variable in a workflow. Similar to a propertyset, it can be accessed anywhere in the workflow, for as long as it is active. The difference however is that a register is not a persistent value, it is a calculated value that is created or looked up anew with every workflow invocation.

How is this useful? Well, in our document management system, it would be useful to have a 'document' register that allows functions, conditions, and scripts to access the document being edited.

Registers are placed in the transientVars map, and so can be accessed from almost anywhere.

Defining a register is very similar to defining a function or condition, with one important difference. Since a register is not invocation-specific (ie, it doesn't care about the current step, or what the inputs are; all it does is expose something), it does not have access to transientVars.

Registers must implement the Register interface, and are specified at the top of the workflow definition, before initial actions.

For our example, we'll specify one of the built-in registers, LogRegister. This register simply adds a 'log' variable that allows you to log messages using Jakarta's commons-logging. The advantage of using it is that it will also add the instance ID to every log message.

<registers>
  <register type="class" variable-name="log">
    <arg name="class.name">
      com.opensymphony.workflow.util.LogRegister
    </arg>
    <arg name="addInstanceId">true></arg>
    <arg name="Category">workflow></arg>
  </register>
</registers>

Now we have a 'log' variable available, we can use it in an inline script by adding another pre-function:

  <function type="beanshell">
    <arg name="script">transientVars.get("log").info("executing action 2")></arg>
  </function>

The logging output will now contain the workflow process instance ID (piid).

This tutorial has hopefully illustrated the some of the major ideas in OSWf. You should feel comfortable enough now with the API and descriptor syntax to explore further on your own. There are many more advanced features that are not mention here, like splits, joins, nested conditions, auto steps, and others. Feel free to browse the manual to get a stronger grasp on what is available.