Workflow Developer's Guide

By Lars Pind on 30 August 2000.

ACS Documentation : Workflow : Workflow Developer's Guide


This document deals with how to use the services of the workflow package in your own package.

What Do I Gain?

Given the formal specification of a workflow, the workflow package will keep track of what tasks are to be performed, notify via email the people that should perform them, and make sure that the same task doesn't accidentally get executed twice by different people.

It'll also provide your users with a unified user-interface to help them keep track the tasks they are supposed to perform. This user-interface also acts as the single place to go for information relating to the task, such as inputs and aids in performing the specific task, a log of all prior activity on the case including comments, and a visual overview over the case.

It also provides you with an interface for analyzing the performance of your workflow. You'll be able to spot bottlenecks, find overdue tasks, etc.

Finally, as the package evolves based on input from you and other users, the features above will grow stronger, and more will be added (see future plans).

What Does It Take?

The workflow process is centered around an object. In an insurance company, that could be an insurance claim; if you're writing the ticket-tracker, it would be the ticket itself; etc. So you need to define that object first, creating tables, etc. as necessary.

From there on, there are four steps involved in taking advantage of the services of the workflow package in your application.

  1. Define your workflow. You need to formally specify your workflow process definition and load that into the database. This is as simple or as hard as the actual workflow you're dealing with. If the workflow for your particular application is simple, then formalizing it for use with the workflow package is going to be simple. If the workflow for your application is complex, then formalizing it is going to be complex. There is a very easy-to-use web-based interface to help you formalize simpler workflows.

  2. Add callbacks. At a minimum, you probably want to customize the information on the page displaying a single task to display the information about your object, e.g. the ticket. For more flexibility and control, the workflow offers callbacks that allows you to control assignments, deadlines, timeouts, etc., programmatically, as well as hooks that allow you to execute code when tasks get ready to be executed, or when they are executed.

  3. Add the trigger. You'll need a trigger to ensure that whenever a new object of your type (e.g. ticket or insurance claim) is created, a case is initialized for that object. This is a copy-and-paste one-liner.

  4. Integrate the user-experience. Your package probably wants some parameter and admin pages, as well as some end-user pages. These should be integrated with the user-interface offered by the workflow package.

Defining Your Process

In order to get a process definition in a redistributable format (a sequence of DML statements), you'll need to code it by hand. We're working on an import/export feature that will let you distribute your process in an XML-based format.

You can go a long way by starting with the Simple Process Wizard to define the basic steps of your process, then, if necessary, use the Advanced Process Builder, to add more advanced features. With this already-defined process as a starting point, it'll be relatively easy to translate it into DML. If you haven't already, this it the time to familiarize yourself with the conceptual model underlying the workflow package.

Then you simply translate the process into SQL (or more correctly, DDL and DML), like this (look at some of the samples to see how it's done):

  1. Create a table to hold cases of your workflow. Each workflow definition is an ACS kernel object type. Each object type in ACS kernel needs its own table to hold a row for each object of this type. So you need to create this table, which will typically only have one column, namely the case_id, referencing wf_cases. Here's how that could look:
    create table wf_expenses_cases (
      case_id               integer primary key
                            constraint wf_expenses_cases_case_fk
                            references wf_cases(case_id)
                            on delete cascade
    );
    
  2. Create the workflow key. Once the table is defined, we can declare the object type, while at the same time providing a name and a description for the workflow:
    declare
        v_workflow_key varchar(40);
    begin
        v_workflow_key := workflow.workflow_create(
            workflow_key  => 'expenses_wf', 
            pretty_name   => 'Expense Authorization', 
            pretty_plural => 'Expense authorizations', 
            description   => 'Workflow for authorizing employee''s expenses on the company''s behalf', 
            table_name    => 'wf_expenses_cases'
        );
    end;
    /
    show errors
    
    The table_name argument is the name of the table you just defined. The workflow_key also becomes the name of the object type (primary key in acs_object_types). Don't forget the on delete cascade if you want to be able to delete cases.

  3. Create places. A simple insert statement per place:
    insert into wf_places(place_key, workflow_key, place_name, sort_order) 
        values ('start', 'expenses_wf', 'Initial state', 1);
    
    There are two special places: start and end. There must be one of each, and they must be named so. The sort_order column has no influence on the execution on the workflow, it's only there so we can display the places and transitions in some logical order, when we can do nothing better.

  4. Create transitions. A simple insert statement per transition:
    insert into wf_transitions(transition_key, transition_name, workflow_key, sort_order, trigger_type) 
        values ('assign', 'Assign users to approval', 'expenses_wf', 1, 'user');
    
    The trigger type can be user, automatic, time or message. You can read about those in the conceptual guide.

  5. Create arcs. The arcs are also pretty straight-forward to create. It helps to sort the arcs around transitions like this:
    -- transition: assign
    -- in
    insert into wf_arcs(workflow_key, transition_key, place_key, direction) 
        values ('expenses_wf', 'assign', 'start', 'in');
    -- out
    insert into wf_arcs(workflow_key, transition_key, place_key, direction) 
        values ('expenses_wf', 'assign', 'assignments_done', 'out');
    
    Keep in mind that every transition must have at least one arc going in and one arc going out. All arcs go either from a place to a transition, or from a transition to a place. You can't have an arc from a place to a place or from a transition to a transition.

    Outgoing arcs can have guards, which are predicates that determine whether the arc is 'really there' in a specific situation. Usually the guard will depend on the values of one or more workflow attributes (see conceptual guide). You'll learn how to add these in the next section, Adding Callbacks.

  6. Create attributes. For attributes, we use features of the ACS kernel, so you need to use the API like this:
    declare
        v_attribute_id acs_attributes.attribute_id%TYPE;
    begin
        v_attribute_id := workflow.attribute_create(
            workflow_key   => 'expenses_wf',
            attribute_name => 'supervisor_ok',
            datatype       => 'boolean',
            wf_datatype    => 'none',
            pretty_name    => 'Supervisor Approval',
            default_value  => 'f'
        );
    
        insert into wf_transition_attribute_map
            (workflow_key, transition_key, attribute_id, sort_order) 
        values
            ('expenses_wf', 'supervisor_approval', v_attribute_id, 1);
    
    end;
    /
    show errors;
    
    First you define the attribute. The wf_datatype is a hack that should go away soon. Currently it's only used for specifying that an attribute holds values of type party (useful for manual assignments). In that case, you set datatype to number and wf_datatype to party. We'll add more subtypes as needed, so please let use know what you need.

    When you've defined the attribute, you usually also want to specify when this attribute should be set. You do that by inserting a row into wf_transition_attribute_map, saying that this attribute should be part of the output for this transition (task). The sort_order here is used to determine the order the attributes are presented in on the form the user fills in when finishing the task.

Adding Callbacks

We use callbacks for several things: You can write the code that implements your callback yourself, or you can use the predefined standard callbacks provided in the wf_callback package. To make it easier to adopt workflow, we'll try to add predefined callbacks for all the common cases that we can think of, so please let us know what callback you're missing.

If you want to write your callbacks yourself, you must adhere to the pre-specified procedure/function signature. These are listed in Appendix A in this document, We'll but the authoritative reference for these are the sql/wf-core-create.sql SQL file.

Generally, the callbacks take the arguments necessary to uniquely identify the object (arc, transition, task) they're attached to, the case_id, and a custom_arg, which makes it possible to re-use the same callback for similar purposes.

To instruct the workflow engine to invoke your callbacks at the specified points in time, set the relevant columns in wf_context_transition_info. You set the foo_callback column to the name of the PL/SQL or Tcl procedure/function you want called, and foo_custom_arg to the value you want passed in to this procedure/function as the last, custom argument.

Note on the special built-in guard #: There's a special built-in guard, #. If, instead of a PL/SQL function name, you set the guard_callback to the literal '#', that is taken to mean "true if no real guards on arcs going out of this transition were true". This is very useful for or-splits, where you use a PL/SQL function for the one guard, and the # symbol for the other. This way, if the real guard is true, that arc is traversed, otherwise the other arc is traversed.

The semantics generalizes to more than two arcs and more than one #. First all the arcs with real guards are tried, and all the arcs where the guard evaluates to true are traversed. If no real guard evaluated to true, then all the arcs with a # guard are traversed, otherwise none of them are traversed.

A few examples of callbacks:

A Guard
insert into wf_arcs(
    workflow_key, transition_key, place_key, direction, 
    fguard_callback
) values (
    'expenses_wf', 'and_join', 'ready_to_buy', 'out', 'wf_expenses.guard_both_approved_p'
);

insert into wf_arcs(
    workflow_key, transition_key, place_key, direction, guard_callback
) values (
    'expenses_wf', 'and_join', 'end', 'out', '#'
);
Here we have two arcs, one with a real guard and the other with the # guard, thus building an or-split. The function implementing the guard (wf_expenses.guard_both_approved_p) must be defined elsewhere.

An Assignment Callback and a Hold-Timeout Callback
insert into wf_context_transition_info (
    context_key, 
    workflow_key, 
    transition_key, 
    assignment_callbackf, 
    hold_timeout_callback, 
    hold_timeout_custom_arg
) values (
    'default', 
    'expenses_wf', 
    'supervisor_approval', 
    'wf_expenses.assign_supervisor_approval', 
    'wf_callback.time_sysdate_plus_x', 
    1/24
);
Here, for the assignment callback, we presumably have a PL/SQL function, wf_expenses.assign_supervisor_approval, which returns the user_id of the supervisor for a user. For the hold-timeout callback, we're using one of the predefined callbacks, wf_callback.time_sysdate_plus_x, which returns Oracle sysdate plus the value given in hold_timeout_custom_arg.
Note on contexts: Contexts are what separates the formal definition of a workflow process from its implementation in an actual context, such as a company or a department within that company. You can also have multiple contexts for the same workflow, allowing you to use the same basic workflow in different departments.

Static assignments of tasks to users or groups (e.g. assign the task 'Pay bill' to the billing department) will definitely vary from implementation to implementation. Other things, like the callback when a task is finished, may better belong to the workflow definition itself. We're hoping for some more real-world experience on this, so please share your experiences.

Add the Trigger

Finally, you'll want to add a trigger on the table holding your objects (e.g. insurance claims or tickets), that calls workflow_case.new when a new row is inserted into this table. (More details later when I have done this myself.)

Static Assignments

Static assignments (saying "this task must always be performed by a member of the billing department") will often depend on the context. If your application creates its own groups to use for this kind of thing, then you can make the static assignments when you define the workflow.

Most of the time, though, you want your users to be able to determine this at run-time. There will be a user-interface for this in the final release, but it is not done yet.

Appendix A. Callback Signatures

Guards (PL/SQL)
function name(
    case_id           in number, 
    workflow_key      in varchar, 
    transition_key    in varchar, 
    place_key         in varchar, 
    direction         in varchar, 
    custom_arg        in varchar
) return char(1);
Transition Enable/Fire (PL/SQL)
procedure name(
    case_id in number, 
    transition_key in varchar, 
    custom_arg in varchar
);
Task Assignment (PL/SQL)
procedure name(
    task_id in number, 
    custom_arg in varchar
)
This procedure must insert rows into the wf_task_assignments table.

Timed Transition Trigger Time (PL/SQL)
function name(
    case_id in number, 
    transition_key in varchar, 
    custom_arg in varchar
) return date;
Task Deadline Date (PL/SQL)
function name(
    case_id in number, 
    transition_key in varchar, 
    custom_arg in varchar
) return date;
Task Timeout Date (PL/SQL)
function name(
    case_id in number, 
    transition_key in varchar, 
    custom_arg in varchar
) return date;
Panels for Task HTML Page (Tcl)
This is a callback that is used to UI purposes, and since the UI is generated by AOLserver/Tcl, this callback will get called from there. The procedure will get called like this:
name -task_info [wf_task_info $task_id]
Look at wf_task_info documentation to see what information this variable will contain.

lars@pinds.com
Last Modified: 2001/01/21 00:36:46