Skip to main content
Skip table of contents

Workflows

This lesson introduces the concept of workflows. By completing this module, you will learn:

  • What a workflow is and why to use one
  • How to initiate a workflow
  • The types of steps supported in a workflow and their purpose

Key concepts

Workflows are a series of decisions and steps that define a business process. Workflows are invoked by events in the business model, and their process token ("this") is an instance of a business class. Workflow steps can run script that interact with the business model, send emails, invoke rule sets, write to log files, wait for timer events, invoke parallel processing of queues, etc.

The workflow engine is aware of all activity in the business model. Workflows can be used for very complicated processes that support long running, persisted operations. Workflows are sometimes used simply as a visual programming tool in the design of your business logic.

Create a workflow

In this lesson, you add a Tentative Delete feature to the training:Person class. When someone calls the delete event on a training:Person instance (or any object instance), the instance is removed from the system. With this new feature, a new event submitDelete flags an instance for deletion. The instance is removed only if that flag remains after a period of time. Another event, cancelDelete, allow someone to cancel the deletion by removing the delete flag. These events are managed using a workflow.

Add events to the training:Person class

Add two events to the training:Person class that are used to trigger functionality in the new workflow:

  1. Open the training:Person class and click the Events tab.
  2. Add two new events, submitDelete and cancelDelete. For each event, add a new, empty main action.These two events are left empty. They exist simply to trigger a workflow.

Create the workflow

Create the workflow and add the steps necessary to implement the new functionality.

To create the workflow:

  1. In the Business Model layer, click the Workflows tab.
  2. Create a new workflow called TentativeDelete.
  3. In the Properties view for the new workflow, set the following attributes:
    • Class: training:Person
    • Event: submitDelete
    • Variables: deleteFlag
    The above configuration triggers the TentativeDelete workflow whenever the submitDelete event is invoked. The deleteFlag variable is available to all steps of the workflow.

Add an action:

  1. In the workflow diagram editor, if the Palette is not visible click the left-pointing arrow at the top of the right border of the editor window to display it.
  2. Select the Action tool from the Steps folder of the Palette and click on the workflow line.
  3. In the Property Editor tab, set the following properties in the General and Script tabs:
    • Name: init
    • Caption: Initialize delete flag
    • Script:

      CODE
      (set! deleteFlag #t)

Add a fork with an action and a timeout:

  1. Select the Fork tool from the Flow folder of the Palette and add it after the init step.
  2. Set the following properties for the Fork:
    • Name: timedWait
    • Type: any
  3. Add an Action inside the Fork.
  4. Set the following properties:
    • Name: cancelDelete
    • Caption: Handle Cancel event
    • Description: This script fires if Person'cancelDelete is invoked. This sets deleteFlag to false.
    • Event: cancelDelete
    • Script:

      CODE
      (set! deleteFlag #f)

Add a timeout

  1. Select the Timeout tool from the Steps folder of the Palette.
  2. Add the Timeout inside the timedWait Fork.
  3. Set the following properties:
    • Name: deleteDelay
    • Caption: Delay before deleting
    • Description: Timer fires if Person'cancelDelete doesn't happen before timeout is complete.
    • Value: 10000 (the wait time in milliseconds)

Add a decision:

  1. Select the Decision tool from the Flow folder of the Palette and add it after the timedWait Fork.
  2. Set the decision's Name to checkDeleteFlag.

Add an action to perform the deletion:

  1. Add another Action after the Decision with the following properties:
    • Name: doDelete
    • Caption: Invoke delete
    • Script:

      CODE
      (this'delete)

Add connectors that determine the flow of the workflow:

  1. Select the Connector tool from the Flow folder of the Palette and connect the checkDeleteFlag decision node to the Invoke delete action.
  2. Set the Connector Branch properties to:
    • Name: deleteTrue
    • Caption: Delete set
    • Condition

      CODE
      (= deleteFlag #t)
  3. Add another Connector between the checkDeleteFlag decision node and the end of the workflow. Set the Connector Branch properties to:
    • Name: deleteFalse
    • Caption: Delete cancelled
    • Condition:

      CODE
      (= deleteFlag #f)
  4. Add another Connector between Invoke delete (the doDelete action node) and the end of the workflow.

Add notes to the workflow:

  1. Right-click a blank area of the workflow canvas and select Create Note.
  2. Set the text of the note to Tentatively deletes a Person. When training:Person'submitDelete is invoked, the workflow starts.
  3. Right-click the init action and select Create Note. The text of the note is the node's Description.
  4. Right-click each node inside the fork and select Create Note. Position the notes so they do not overlap other nodes.
  5. Save your work.
  6. Your workflow should look similar to the following:

    TentativeDelete

Test the workflow

The timers that the workflow depends on do not work in the Scheme Minimal Console.

Timers use services provided by an application server, so you must use the Server Console rather than the Minimal Console.

To test the workflow:

  1. Run the Server Console.
  2. In a scratchpad enter the following code.

    CODE
    ; used to create the test Person record: Bob A Event
    (define pCreate
       (lambda ()
          (begin-transaction
             (training:Person'new
                (: firstName "Bob")
                (: initials "A")
                (: lastName "Event")
                (: primaryLanguage (training:LanguageEnum'get'ENGLISH))
             )
           )
        )
    )
    ; used to call submitDelete on the "Event" person
    (define pSubmit
       (lambda ()
          (begin-transaction
             ((read-instance training:Person '() '(= lastName "Event") '())'submitDelete)
          )
       )
    )
    ; used to call cancelDelete on the "Event" person
    (define pCancel
       (lambda ()
          (begin-transaction
             ((read-instance training:Person '() '(= lastName "Event") '())'cancelDelete)
          )
       )
    )
    ; used to query if an "Event" person exists
    (define pLookup
       (lambda ()
          (let ((p (read-instance training:Person '() '(= lastName "Event") '())))
             (if (null? p)
                "Person not found!!!"
                (string-append (p'firstName) " " (p'lastName) " is still here!!!")
             )
          )
       )
    )
    ; delete all instances of Person with last name "Event"
    (define pCleanUp
       (lambda ()
          (begin-transaction
             (for-each
                (lambda (x)
                   (x'delete)
                )
             (training:Person'read '() '(= lastName "Event") '() -1 0 #f)
           )
        )
      )
    )

    Info

    Any work that is transaccional in nature must be wrapped in a transaction. If you ever get an Error: err.runtime.txMandatory when using the Server Console, wrap what you are trying to execute in a (begin-transaction ...) statement.

  3. Select the code and execute it in the Server Console.
    The code consists of function definitions. When you run the code, you create the functions pCreate, pSubmit, pCancel, pLookup, and pCleanup. In the following steps, you execute the functions to test the workflow.
  4. Create a person instance by executing the pCreate function:

    CODE
    > (pCreate)
  5. Verify the person exists by executing the pLookup function:

    CODE
    > (pLookup)
    CODE
    ; 13:56:42,085 DEBUG [Person] (NexJ-ContainedProcess) Invoking Event
     training:Person.read(attributes, where, orderBy, count, offset, xlock)
    ; 13:56:42,085 DEBUG [SQLAdapter] (NexJ-ContainedProcess) select A.id, A.locking
     from NJTrainingEntity A where A.lastName = ? and A.classCode = ? for read only
    ; 13:56:42,085 DEBUG [Adapter] (NexJ-ContainedProcess) Bind[0] = 'Event'
    ; 13:56:42,085 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Bind[1] = 'PERSON'
    ; 13:56:42,086 DEBUG [SQLAdapter] (NexJ-ContainedProcess) SQL execution time: 1
     ms
    ; 13:56:42,086 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Retrieved 1
     instance(s) of Person in 0 ms
    
    ; "Bob Event is still here!!!"SQL
  6. Test the delete path of the workflow by submitting a delete request using the pSubmit function.

    CODE
    > (pSubmit)
  7. Wait at least 10 seconds. You will see some debug statements scroll in the console. Verify the person is now deleted using the pLookup function.

    CODE
    > (pLookup)
    CODE
    14:00:13,458 DEBUG [Person] (NexJ-ContainedProcess) Invoking Event
     training:Person.read(attributes, where, orderBy, count, offset, xlock)
    ; 14:00:13,458 DEBUG [SQLAdapter] (NexJ-ContainedProcess) select A.id, A.locking
     from NJTrainingEntity A where A.lastName = ? and A.classCode = ? for read only
    ; 14:00:13,458 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Bind[0] = 'Event'
    ; 14:00:13,458 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Bind[1] = 'PERSON'
    ; 14:00:13,459 DEBUG [SQLAdapter] (NexJ-ContainedProcess) SQL execution time: 1
     ms
    ; 14:00:13,460 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Retrieved 0
     instance(s) of Person in 0 ms
    
    ; "Person not found!!!"
  8. Create a person instance again by executing the pCreate function:

    CODE
    > (pCreate)
  9. Verify the person exists by executing the pLookup function:

    CODE
    > (pLookup)
    CODE
    ; 14:04:09,597 DEBUG [Person] (NexJ-ContainedProcess) Invoking Event
     training:Person.read(attributes, where, orderBy, count, offset, xlock)
    ; 14:04:09,597 DEBUG [SQLAdapter] (NexJ-ContainedProcess) select A.id, A.locking
     from NJTrainingEntity A where A.lastName = ? and A.classCode = ? for read only
    ; 14:04:09,597 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Bind[0] = 'Event'
    ; 14:04:09,598 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Bind[1] = 'PERSON'
    ; 14:04:09,599 DEBUG [SQLAdapter] (NexJ-ContainedProcess) SQL execution time: 1
     ms
    ; 14:04:09,599 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Retrieved 1
     instance(s) of Person in 0 ms
    
    ; "Bob Event is still here!!!"
  10. Try the cancel path.
    First issue:

    CODE
    (pSubmit)

    then before 10 seconds are up issue:

    CODE
    (pCancel)

    After 10 seconds during which several debug statements are logged to the console, verify that the person still exists:

    CODE
    > (pLookup)
    CODE
    ; 00:07:00,708 DEBUG [Person] (NexJ-ContainedProcess) Invoking Event
     training:Person.read(attributes, where, orderBy, count, offset, xlock)
    ; 00:07:00,709 DEBUG [SQLAdapter] (NexJ-ContainedProcess) select A.id, A.locking
     from NJTrainingEntity A where A.lastName = ? and A.classCode = ? for read only
    ; 00:07:00,709 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Bind[0] = 'Event'
    ; 00:07:00,709 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Bind[1] = 'PERSON'
    ; 00:07:00,711 DEBUG [SQLAdapter] (NexJ-ContainedProcess) SQL execution time: 2
     ms
    ; 00:07:00,711 DEBUG [SQLAdapter] (NexJ-ContainedProcess) Retrieved 1
     instance(s) of Person in 0 ms
    
    ; "Bob Event is still here!!!"

Create a unit test

Create a unit test to ensure that the workflow continues to work successfully even after future model changes.

To create the unit test:

  1. Create a new unit test named Person.

  2. In the Overview tab, set the following properties:

    • Description: Unit tests for the Person class

    • Dump: UnitTest

    • Mode: sequential

  3. Add a test case:

    • Name: cancelDelete

    • Description: Test cancelling a tentative delete

    • Script:

      CODE
      (define p '())
      (training:Person'new (: firstName "Bob") (: initials "A") (: lastName "Event") (: primaryLanguage (training:LanguageEnum'get'ENGLISH)))
      (commit)
      (reset-context)
      
      ; submit the delete
      ((read-instance training:Person '() '(= lastName "Event") '())'submitDelete)
      (commit)
      
      ; cancel the delete
      ((read-instance training:Person '() '(= lastName "Event") '())'cancelDelete)
      (commit)
      (reset-context)
      
      ; ensure that the person still exists
      (set! p (read-instance training:Person '() '(= lastName "Event") '()))
      (assert-equal "Bob" (p'firstName))
  4. Run the unit test and ensure that the test case successfully completes.

Update the revision and publish the model

Since you did not change the database schema in this lesson, you do not edit the Upgrade file, upgrade the UnitTest dump file, or upgrade the database. The model's version number that is used to identify data source schema remains the same.

However, you should increment the Model Revision number of the project. Updating the revision number of the model indicates that the metadata has changed since the previous "release"..

  • Update the model's revision number (e.g. from 9.0.0.5 to 9.0.0.6).
  • Publish the model


JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.