Overview

Triggers provide a great deal of customization and flexibility to process design. These examples provide some ideas for more advanced uses of triggers.

Aborting New Cases Under Certain Circumstances

A trigger could be written to abort an existing case under certain circumstances. For instance, suppose there is a process where users should not open a new case if there is already an existing case which hasn't yet been completed (or canceled). The following trigger code could be inserted before the first step in the initial task to lookup whether there is already a case assigned to the current logged-in user whose APP_DELEGATION.DEL_THREAD_STATUS field is equal to 'OPEN', meaning that the task hasn't been completed (or canceled) yet.

//assign system variables to PHP variables so can be inserted in strings  
$user = @@USER_LOGGED;
$process = @@PROCESS;
//Query to find any uncompleted steps assigned to logged-in user
$query = "SELECT * FROM APP_DELEGATION WHERE PRO_UID='$process' AND ".
         "USR_UID='$user' AND DEL_THREAD_STATUS='OPEN'";
$res = executeQuery($query);

//if any open tasks are are found, assigned to user
if (is_array($res) and count($res) >= 2){
   $c = new Cases();
   $c->removeCase(@@APPLICATION);
   $query = "SELECT * FROM APP_DELEGATION WHERE PRO_UID='$process' AND ".
            "USR_UID='$user' AND DEL_THREAD_STATUS='OPEN'";
   $res = executeQuery($query);
   $casesId = $res[1]['APP_UID'];
   $resExisting = executeQuery("SELECT * FROM APPLICATION WHERE ".
                               "APP_UID='$casesId'");
   $caseNo = $resExisting[1]['APP_NUMBER'];
   $caseId = $resExisting[1]['APP_UID'];
   $delindex = executeQuery("SELECT DEL_INDEX FROM APP_DELEGATION ".
                            "WHERE APP_UID='$caseId'");
   $caseIdx = $delindex[1]['DEL_INDEX'];

   //Finally display message to user to first complete other case
   //and redirect to the existing case
$g = new G();
$g->SendMessageText("Case Aborted. Please complete or cancel ".
      "this Case #$caseNo before starting a new case", 'WARNING');
   G::header("Location: cases_Open?APP_UID=$caseId&".
             "DEL_INDEX=$caseIdx&action=sent");
   die();    
}

If there is, then the new case which was just started will be deleted and a warning message will be displayed along with the summary of the existing case that the user should first complete.

Workaround for Global Variables

ProcessMaker does not have global variables, which can be used for all cases and processes. The best workaround for a global variable is to store values in a PM Table or an external database, and then look it up when needed with executeQuery(). When needing to permanently change the value of the variable, write to the database.

For example, a PM Table named "GLOBALS" could be created with the fields "VARIABLE" and "VALUE". To store the names of the managers of a company, the following data could be stored in the "GLOBALS" PM Table:

VARIABLE VALUE ---------------- ------------------------------- CEO Jane Doe COO John Smith CFO Billy Bragg SALES_MANAGER Sarah Quimby SUPPORT_MANAGER Sally Simpson

Then use the following query to look up the "CEO" global variable:

$result = executeQuery("SELECT VALUE FROM PMT_GLOBALS WHERE VARIABLE='CEO'");
if (is_array($result) and count($result) > 0)
   $ceo = $result[1]['VALUE'];
else
   $ceo = '';

To set the "CEO" global variable to "Mary Marvin":

executeQuery("UPDATE PMT_GLOBALS SET VALUE='Mary Marvin' WHERE VARIABLE='CEO'");

Note: In ProcessMaker 3.0+, PM Tables have "PMT_" prepended to their names in the wf_<WORKSPACE> database.

Global Counters for Cases

PM Tables can be used to implement global counters that can work across all cases. For example, if needing to keep a counter of the number of cases for each department in an organization, create a PM Table named "COUNTERS" with the fields "VARIABLE" and "VALUE" with the following contents:

VARIABLE                  VALUE
------------------------  -------
SALES_COUNTER             0
FINANCE_COUNTER           0
HUMAN_RESOURCES_COUNTER   0

Then, add a trigger to every case to increment the counter and display the number in the counter in a DynaForm when beginning a new case. For example a new case in the Finance department would execute the following trigger before the first step in the first task:

//get the current value of the FINANCE_COUNTER and increment it:
$result = executeQuery("SELECT VALUE FROM PMT_COUNTERS WHERE VARIABLE='FINANCE_COUNTER'".
                       "") or die("Error accessing the COUNTERS PM Table!");
if (is_array($result) and count($result) > 0) {
   @%counter = $result[1]['VALUE'] + 1;
   //write the new counter value to the database:
   executeQuery("UPDATE PMT_COUNTERS SET VALUE=" . @%counter . " WHERE
    VARIABLE='FINANCE_COUNTER'"
);
}
else
   @%counter = 1;

Then, add a field named "counter" to a subsequent DynaForm, so the value of the counter can be displayed to the user.

Note: In this case, the VALUE field is an INTEGER type, so its value doesn't need to be enclosed in single quotation marks like a CHAR or VARCHAR field.

Routing (Derivating) all Parallel Tasks in a Case

A trigger can be used to automatically route (derivate) all the the parallel tasks in a case. For example, if a purchase request is sent to 3 managers as 3 parallel tasks.

Once one manager makes a decision to approve or disapprove the purchase request, the case will automatically route on for all the other managers, so the first manager to make a decision will decide for the others and the case will be removed from the inbox of the other managers. The following trigger is fired after Dynaform of all parallel tasks.

//if set, then a decision was submitted by the manager
if (isset(@@ManagerDecision)) {
    $caseId= @@APPLICATION;
    $userLogged = @@USER_LOGGED;
    $index = @%INDEX;
   
    //Select all the other parallel tasks for the current case that are still open
    $query = "SELECT DEL_INDEX, U.USR_USERNAME FROM APP_DELEGATION AD, ".
             "USERS U WHERE AD.APP_UID = '$caseId' AND AD.DEL_THREAD_STATUS='OPEN' ".
             "AND AD.USR_UID = U.USR_UID AND AD.DEL_INDEX <> $index";
    $threads = executeQuery($query);
   
    if (is_array($threads) and count($threads) > 0) {  
        foreach ($threads as $thread) {
          //Login as the assigned users and route on their tasks
          $sql = "SELECT USR_PASSWORD FROM USERS WHERE ".
                 "USR_USERNAME = '{$thread['USR_USERNAME']}'";  
          $task_user = executeQuery($sql);
          if (is_array($task_user) && count($task_user) > 0) {
            $nextpass = 'md5:' . $task_user[1]['USR_PASSWORD'];
            $sessionId = WSLogin($thread['USR_USERNAME'], $nextpass);
$client = new SoapClient('http://localhost/sysworkflow/en/neoclassic/services/wsdl2');        
            $params = array(array(
                'sessionId'=> $sessionId,
                'caseId'    => $caseId,
                'delIndex'  => $thread['DEL_INDEX']
            ));
            $result = $client->__SoapCall('routeCase', $params);
            if ($result->status_code != 0)
                die("Error routing case: {$result->message}\n");    
          }
        }  
    }
}

Routing Parallel Tasks after Completing a Fixed Number

A process to approve expenses needs at least 3 out of 5 managers to review the expense before a decision can be made. The process has 5 parallel tasks which are assigned to each one of the managers.

The following trigger is fired After Routing of all the parallel tasks (which means that it will be fired after all the steps have been completed).

The trigger code checks to see whether at least two of the other parallel tasks have already been completed. Including the current task, that would mean that three tasks have been completed, so the rest of the parallel tasks will automatically be routed (derivated). The code looks up the assigned users for those tasks and logins as those users and calls the routeCase() web service for each task.

$caseId = @@APPLICATION; //ID for current case
$taskId = @@TASK;

//UIDs of the parallel tasks
$parallelTasks ="'796356201572280bd19ebb5038311779','888702392572b6bbb11fcb4074248244',".
                "'692596435572b6bbb7a3db7095131549','820220947572b6bbbe954d8093262285',".
                "'689544677572b6bbc4bdda8055390856'";

$tasks = executeQuery("SELECT APP_UID, DEL_INDEX, TAS_UID, DEL_THREAD_STATUS, ".
                      "USR_UID FROM APP_DELEGATION WHERE APP_UID='$caseId' ".
                      "AND TAS_UID in ($parallelTasks) and DEL_THREAD_STATUS <> 'CLOSED'");

//If 2 of the parallel tasks are open, then route them
if (count($tasks) == 2){
    $client = new SoapClient('http://localhost/sysworkflow/en/neoclassic/services/wsdl2');
    foreach ($tasks as $task) {  
      if ($task['DEL_THREAD_STATUS'] == 'OPEN') {
        $result = executeQuery("SELECT USR_USERNAME, USR_PASSWORD FROM USERS ".
                               "WHERE USR_UID='{$task['USR_UID']}'");
        $params = array(array(
            'userid'   => $result[1]['USR_USERNAME'],
            'password' => 'md5:' . $result[1]['USR_PASSWORD']
        ));
        $result = $client->__SoapCall('login', $params);
        if ($result->status_code == 0)
            $sessionId = $result->message;
        else
            die("Unable to connect to ProcessMaker.\nError Number: {$result->status_code}\n".
               "Error Message: {$result->message}\n");
        $params = array(array(
            'sessionId' => $sessionId,
            'caseId'    => $caseId,
            'delIndex'  => $task['DEL_INDEX']
        ));
        $result = $client->__SoapCall('routeCase', $params);
        if ($result->status_code != 0)
            die("Error deriving case: {$result->message}\n");  
      }
    }
}

Displaying a List of Cases in a Grid

A grid with information and links to cases can be displayed in a DynaForm. For example, to display a list of cases for the currently logged in user in a grid, first create a grid named "casesGrid" with the fields "caseNumber" (textbox), "processName" (textbox), "taskName" (textbox), "dueDate"(date), "linkToCase" (link), "urlForCase" (hidden), and "labelForCase" (hidden).

Then, fire a trigger before DynaForm to populate the grid, with code like this:

@=casesGrid = array();
$userId = @@USER_LOGGED;
$q =  "SELECT APP_UID, DEL_INDEX FROM APP_DELEGATION WHERE USR_UID='$userId'".
      " and DEL_THREAD_STATUS='OPEN'";
$cases = executeQuery($q);
if (is_array($cases) and count($cases) > 0) {
   for ($i = 1; $i <= count($cases); $i++) {
      $caseId = $cases[$i]['APP_UID'];
      $caseIndex = $cases[$i]['DEL_INDEX'];
      $oCase = new Cases();
      $aCaseInfo = $oCase->loadCase($caseId, $caseIndex);
      //look up the Task Name and Process Name:
      $lang = @@SYS_LANG;
      $result = executeQuery("SELECT CON_VALUE FROM CONTENT WHERE ".
                             "CON_ID = '{$aCaseInfo['PRO_UID']}' AND ".
                             "CON_CATEGORY='PRO_TITLE' AND CON_LANG='$lang'");
      $processName = $result[1]['CON_VALUE'];

      $result = executeQuery("SELECT CON_VALUE FROM CONTENT WHERE ".
                             "CON_ID = '{$aCaseInfo['TAS_UID']}' AND ".
                             "CON_CATEGORY='TAS_TITLE' AND CON_LANG='$lang'");

      $taskName = $result[1]['CON_VALUE'];
      @=casesGrid[$i] = array(
         "caseNumber" => $aCaseInfo['APP_NUMBER'],
         "processName" => $processName ,
         "taskName" =>  $taskName,
         "dueDate" =>  $aCaseInfo['DEL_TASK_DUE_DATE'],
         "linkToCase" => '',
         "urlForCase" => "../cases/cases_Open?APP_UID=$caseId&DEL_INDEX=$caseIndex",
         "labelForCase" => $aCaseInfo['APP_TITLE']
      );
   }
   @=casesGrid = orderGrid(@=casesGrid, 'dueDate', 'DESC');
}                          

Then, add the following JavaScript code to the DynaForm to set the links in the casesGrid:

var totalRows = $("#casesGrid").getNumberRows();
  for (var i = 1; i <= totalRows; i++) {
      var url = $("#casesGrid").getValue(i, 6);
      $("#casesGrid").setValue(url, i, 5);
  }

Run the case, and the trigger will display a list of all the open cases for the current user:

Skipping the Next Task in a Case

If inside the current task, the rest of the steps in that task can be skipped by calling the PMFDerivateCase() function in a trigger. However, the PMFDerivateCase() function can't be called for tasks to which the current logged-in user is not assigned. Moreover, PMFDerivateCase() can't be called for tasks which don't yet have a record in the wf_<WORKSPACE>.APP_DELEGATION table and whose DEL_THREAD_STATUS field does not equal "OPEN". For these reasons, PMFDerivateCase() cannot be used to skip the next task in a case.

Instead, to skip the next task, create a trigger which fires After Derivation/Routing of the previous task. After derivation, a record has already been created in the wf_<WORKSPACE>.APP_DELEGATION table, so the assigned user and the delegation index for the next task can be looked up with executeQuery(). Also look up the MD5 hash for that user's password in the database. Then, login as that next assigned user with the login() web service and then call routeCase() to route through the next task.

For example:

$caseId = @@APPLICATION;
//lookup the user assigned to the next task in the case
$query = "SELECT AD.DEL_INDEX, U.USR_USERNAME FROM APP_DELEGATION AD, ".
         "USERS U WHERE AD.APP_UID='$caseId' AND ".
         "AD.DEL_INDEX=(SELECT MAX(DEL_INDEX) FROM APP_DELEGATION ".
         "WHERE APP_UID='$caseId') AND AD.USR_UID=U.USR_UID";

$result = executeQuery($query);
$nextUser = $result[1]['USR_USERNAME'];
$nextIndex = $result[1]['DEL_INDEX'];

//lookup the md5 hash for the password
$result = executeQuery("SELECT USR_PASSWORD FROM USERS WHERE USR_USERNAME='$nextUser'");
$nextPass = 'md5:' . $result[1]['USR_PASSWORD'];
 
$client = new SoapClient('http://localhost/sysworkflow/en/neoclassic/services/wsdl2');
$params = array(array('userid' => $nextUser, 'password' => $nextPass));
$result = $client->__SoapCall('login', $params);

if ($result->status_code == 0)
    $sessionId = $result->message;
else
    die("Unable to connect to ProcessMaker.\nError Message: {$result->message}");
 
$params = array(array(
   'sessionId'=> $sessionId,
   'caseId'    => @@APPLICATION,
   'delIndex'  => $nextIndex
));
$result = $client->__SoapCall('routeCase', $params);
if ($result->status_code != 0)
    die("Error routing case: {$result->message}\n");

If needing to execute this code in the middle of the previous task, first call PMFDerivateCase(), then execute the rest of the above code. At the end, call G::header() to redirect to the cases list and then call die(), to stop the previous task from trying to continue onto the next step.

PMFDerivateCase(@@APPLICATION, @%INDEX);

//insert above code here

G::header("location: casesListExtJs"); //if using version 2.0 +
die();

Note: This code will have problems if the next task is a parallel task, because its index number might not be the maximum index number. In that case, search for a particular task ID in the APP_DELEGATION table. Task IDs can be found with the taskList() function.

$query = "SELECT AD.DEL_INDEX, U.USR_USERNAME FROM APP_DELEGATION AD, ".
         "USERS U WHERE AD.APP_UID='$caseId' AND AD.TAS_UID='XXXXXXXXXXXXXXXXXXXXXXX' ".
         "AND AD.DEL_INDEX=(SELECT MAX(DEL_INDEX) FROM APP_DELEGATION WHERE ".
         "APP_UID='$caseId' AND AD.TAS_UID='XXXXXXXXXXXXXXXXXXXXXXX') AND ".
         "AD.USR_UID=U.USR_UID";

Reopening Cases

After a case has been canceled or completed, it is not possible to reopen the case from the ProcessMaker interface. However, it is possible to reopen a case by calling the Cases::ReactivateCurrentDelegation() method to reactivate the last task in the case. Then, use the Cases::updateCase() to change the case's status from "CANCELLED" or "COMPLETED" to "TO_DO", so it will reappear in the user's Inbox. If using the Enterprise Edition, it is also necessary to call the Users::refreshTotal() method to update the user's case counters, so there is one less canceled/completed case and one more inbox case.

For example, a process is created to reopen selected cases which are canceled or completed. This process contains the following DynaForm:

The grid is associated with a grid variable named "casesList" and it contains the following grid fields:

  • checkbox with ID 'reopen'
  • textbox with ID 'caseNo' (disabled mode)
  • textbox with ID 'process' (disabled mode)
  • textbox with ID 'task' (disabled mode)
  • textbox with ID 'user' (disabled mode)
  • textbox with ID 'updated' (disabled mode)
  • textbox with ID 'status' (disabled mode)
  • hidden field with ID 'caseId'
  • hidden field with ID 'index'
  • hidden field with ID 'userId'

This grid is populated by the following trigger code which looks up all the cases with 'COMPLETED' or 'CANCELLED' status in the database and places information about those cases in the "casesList" grid. This trigger is executed before the DynaForm:

$query = "SELECT * FROM APP_CACHE_VIEW AC1 WHERE (AC1.APP_STATUS='COMPLETED' ".
         "OR AC1.APP_STATUS='CANCELLED') AND AC1.DEL_INDEX=(SELECT MAX(DISTINCT ".
         "AC2.DEL_INDEX) FROM APP_CACHE_VIEW AC2 WHERE AC1.APP_UID=AC2.APP_UID) ".
         "ORDER BY AC1.APP_NUMBER DESC ";
$aCases = executeQuery($query);
if (!is_array($aCases))
        die("Error in query: $query");

@=casesList = array();
for($i = 1; $i <= count($aCases); $i++) {
   @=casesList[$i] = array(
      'caseId'  => $aCases[$i]['APP_UID'],
      'index'   => $aCases[$i]['DEL_INDEX'],
      'userId'  => $aCases[$i]['USR_UID'],
      'reopen'  => '0',
      'caseNo'  => $aCases[$i]['APP_NUMBER'],
      'process' => $aCases[$i]['APP_PRO_TITLE'],
      'task'    => $aCases[$i]['APP_TAS_TITLE'],
      'user'    => $aCases[$i]['APP_CURRENT_USER'],
      'updated' => $aCases[$i]['APP_UPDATE_DATE'],
      'status'  => $aCases[$i]['APP_STATUS']
   );
}

A second trigger is created which is set to fire after the DynaForm. Its code loops through the @=casesList grid and reopens each case which is marked by the user.

if (isset(@=casesList) and !empty(@=casesList)) {
   $reopenedCount = 0;
   foreach (@=casesList as $aCase) {
      if ($aCase['reopen'] == '0')
         continue;
               
      $caseId = $aCase['caseId'];
      $index  = $aCase['index'];
      $userId = $aCase['userId'];
      $c = new Cases();
      $result = $c->ReactivateCurrentDelegation($caseId, $index);
               
      $aCaseLoaded = $c->loadCase($caseId);
      $aCaseLoaded['APP_STATUS'] = 'TO_DO';
      $c->updateCase($caseId, $aCaseLoaded);
      $reopenedCount++;
               
      //Uncomment the following code for the Enterprise Edition
      //Update case counters for the user assigned to the case:
      //$u = new Users();
      //$oldStatus = $aCase['status'] == 'COMPLETED' ? 'completed' : 'canceled';
      //$u->refreshTotal($userId, "remove", $oldStatus);
      //$u->refreshTotal($userId, "add", "inbox");
   }
   $g = new G();
   $g->SendMessageText("$reopenedCount cases were reopened.", "INFO");
}

If using the Enterprise Edition, make sure to uncomment the code which instantiates the Users class and calls its refreshTotal() method, so the number of canceled, completed and inbox cases will be accurate for the user assigned the reopened case(s).

To see a sample process containing this code, download and import the process Reopen_Cases-1.pmx (right click on link and select "Save As".

Note: If the number of cases listed for a user becomes inaccurate, the case counters for users can be updated by running the migrate-new-cases-lists ProcessMaker command in a terminal:

LINUX/UNIX

cd /opt/processmaker
./processmaker migrate-new-cases-lists

Windows

cd C:\Users\USERNAME\AppData\Roaming\ProcessMaker-3_X_X_X\processmaker
..\php\php.exe -f processmaker migrate-new-cases-lists

Executing a Task a Variable Number of Times

There are two ways to execute a task a variable number of times in a process, but both methods have their drawbacks.

Repeating a Task with a Loop-Around

Use an exclusive gateway to create a loop-around for the same task in the process map so the task can be repeatedly executed.

In this example, the "Repeated Task" needs to be worked on by a different user of a group each time it executes. For that, set it to use Value Based Assignment, so that a different user can be designated with each pass through the task.

The "Repeated Task" needs to be executed once by every member of a group named "Audit Team" (which has a unique ID of "684661865572a1d7cde5603040620405"). The group "Audit Team" has 3 members.

Set a Trigger to fire in the "Initial Task" which will lookup the members of the "Audit Team". The Trigger creates:

  1. A case variable named @%noRepeats to keep track of the number of times the task has been repeated.
  2. An array named @=membersAuditTeam to hold the unique IDs of the members of the "Audit Team" group.
  3. A case variable named @@currentAuditor to hold the unique ID of the user who is designated to work on the "Repeating Task".
@%noRepeats = 0;
@=membersAuditTeam = array();
@@currentAuditor = '';
G::LoadClass("groups");
$g = new Groups();
$aUsers = $g->getUsersOfGroup('684661865572a1d7cde5603040620405'); //Group's ID
for ($i = 0; $i < count($aUsers); $i++) {
   @=membersAuditTeam[] = $aUsers[$i]['USR_UID'];
}
if (count(@=membersAuditTeam) > 0)
   @@currentAuditor = @=membersAuditTeam[0];  

Set the "Repeated Task" to use Value Based Assignment with the variable @@currentAuditor.

Set the exclusive gateway after "Repeated Task" to use the following conditions:

Finally, set the following Trigger to fire with each pass through the "Repeating Task", so that @%noRepeats will be incremented and @@currentAuditor will be set to the next user in the @=membersAuditTeam array.

@%noRepeats += 1; //increment for each pass through the task
if (@%noRepeats < count(@=membersAuditTeam))
   @@currentAuditor = @=membersAuditTeam[@%noRepeats];

When running the case, the "Repeated Task" will be redirected three times, each time to a different user group before being redirected to the "Final Task".

If a Task May Repeat Zero Times

If the "Repeating Task" needs to have the possibility of not being executed at all (for example, if there are zero members in the "Audit Team"), then an exclusive gateway needs to be added before the "Repeating Task" to skip it.

Then, conditions can be added to the first evaluation routing rule to either go to the Repeating Task or skip to the "Final Task". For example:

The conditions in the second evaluation routing rule will be the same as in the previous example.

Drawbacks

Repeating a task with a loop-around has several drawbacks:

  1. The task will be executed serially, so that it can take a long time for each user to execute the task one by one.
  2. If the repeating task contains a DynaForm, then each new user can change the values of the DynaForm fields with each loop, so the values from the first user might get overwritten by the second user, and the values of the second user might get overwritten by the values of the third user, etc.

A Variable Number of Parallel Tasks

The other way to repeat a variable number of tasks is to create a parallel tasks which will all execute at the same time. Create as many variable tasks as the maximum number of times that the task could possibly be executed.

Note: ProcessMaker includes a parallel marker for tasks in version 3.0.1.4 and later, so the following code example is only necessary if using a previous version of ProcessMaker.

For example, if each parallel task needs to be executed by a member of the "Audit Team", then create as many parallel tasks as the largest number of possible members of the "Audit Team".

Set each of the parallel tasks to use Value Based Assignment. "Parallel Task 1" uses variable @@userForTask1, "Parallel Task 2" uses variable @@userForTask2, "Parallel Task 3" uses variable @@userForTask3 and "Parallel Task 4" uses variable @@userForTask4.

Then, set a Trigger to fire in the "Initial Task" which determines the number of parallel tasks to execute and which users will be designated to work on each parallel task.

For example, the following Trigger fired during the "Initial Task" looks up the members of the "Audit Team" (which has a unique ID of "684661865572a1d7cde5603040620405") and assigns each member to a different parallel task:

G::LoadClass("groups");
$g = new Groups();
$aUsers = $g->getUsersOfGroup('684661865572a1d7cde5603040620405');
@%noMembers = count($aUsers);
if (@%noMembers >= 1)
   @@userForTask1 = $aUsers[0]['USR_UID'];
if (@%noMembers >= 2)
   @@userForTask2 = $aUsers[1]['USR_UID'];
if (@%noMembers >= 3)
   @@userForTask3 = $aUsers[2]['USR_UID'];
if (@%noMembers >= 4)
   @@userForTask4 = $aUsers[3]['USR_UID'];

Set the conditions in the Inclusive Gateway so that only as many parallel tasks are executed as the number of members in the "Audit Team":

Finally, set the steps for each of the parallel tasks. If needing each parallel task to execute the same steps, then keep the following caveats in mind:

  • For DynaForm steps, create a separate DynaForm for each of the parallel tasks. If the same DynaForm is used in all the parallel tasks, then every user can overwrite the data entered by the other users.
    To create separate DynaForms for each parallel task, use the Save As option to create copies. Then go through the copies and rename all the DynaForm fields so they have different names.

For example, if the original DynaForm is named "Audit Form" and it has the fields "accountName", "lastAudit" and "auditResult", then create:

  • Audit Form 1 with the fields: "accountName1", "lastAudit1" and "auditResult1"
  • Audit Form 2 with the fields: "accountName2", "lastAudit2" and "auditResult2"
  • Audit Form 3 with the fields: "accountName3", "lastAudit3" and "auditResult3"
  • Audit Form 4 with the fields: "accountName4", "lastAudit4" and "auditResult4"

In this way, each member of the "Audit Team" can fill out a separate form and won't see the data from the other auditors.

  • For Input Document steps, the same Input Document can be used for all the parallel tasks. Each auditor can add additional files to the existing Input Document. However, if none of the auditors should be able to see the Input Document files from the other auditors, then create a separate Input Document for each parallel task.
  • For Output Document steps, the same Output Document can be used for all the parallel tasks as long as versioning is enabled, so each user doesn't overwrite the Output Documents of the other users.

Sending Grid Information via Email

If the information of a Grid is required to be sent via email follow the steps below:

1. Let's say that we need to send by email the information of a list of users adding in a grid, the id of the grid is GridName and the fields ids are FirstName and LastName respectively

Create a trigger with the following HTML code on which the structure of the grid will be created:

$Grid.= '<table  border="1">';
$Grid.= '<tr>';
$Grid.= '<td style="background-color:#DEDEDE;">First Name</td>';
$Grid.= '<td style="background-color:#DEDEDE;">Last Name</td>';
$Grid.= '</tr>';

foreach (@=GridName as $row) {
   $Grid.= '<tr>';
   $Grid.= '<td>' . $row['FirstName'] . '</td>';
   $Grid.= '<td>' . $row['LastName'] . '</td>';
   $Grid.= '</tr>';
}
$Grid.= '</table>';

It is necessary to create a table structure with a $Grid variable, this variable will be defined to store all the HTML structure, on each line $Grid variable will be concatenated with a previous value to construct the table. On the first part the header of the grid is defined, also cases variables can be defined. As the example above some styles can be used.

On the second part, it is necessary to add a foreach sentence, since it is not possible to know how many rows will be created on the grid, so foreach will allow to go through each line of the grid to create it on the template.

2. Once the structure of the Grid is created, the PMFSendMessage function must be added to define the necessary parameters to send the email:

@@result = PMFSendMessage (
    @@APPLICATION,
    'mail@example.com',
    'mail@example.com',
    'mail2@example.com',
    'mail3@example.com',
    'Grid Example',
    'template.html',
    array('Grid'=>$Grid)
);

Since $Grid is an array it must be send on the last parameter of the function.

Grid will be the name of the variable which will be defined inside template.html, it means that this variable will contain all the grid structure stored at $Grid.

3. Finally create an HTML template named template.html with the following line:

@#Grid

Assign the trigger After Derivation or After Dynaform. Don't forget to configure email notifications to have mails sent successfully.

Create new case and change its status

When a new case is created, its status is automatically set to "DRAFT", so it doesn't appear in the list of cases under Home > Inbox. The Cases::updateCase() method can be used to change the status of the case from "DRAFT" to "TO_DO", so it will appear in the user's Inbox. If using the Enterprise Edition, it is also necessary to call the Users::refreshTotal() method to update the assigned user's case counters, so there is one less draft case and one more inbox case.

The following example creates a new case using the PMFNewCase() function and then changes its status to "TO_DO":

$taskId    = '13440667256aa8f12102541045873124';  //set to starting task's unique ID
$processId = '56583120656aa8ee87197a4069048886';  //set to the process' unique ID
$userLogged = @@USER_LOGGED;                      //set the current user
$aData = array(     //set variables from the current case to send to the new case
       'Name'    => @@clientName,
       'Amount' => @@InvoiceAmount
);
$newCaseId = PMFNewCase($processId, $userLogged, $taskId, $aData);
if ($newCaseId) {
   $c = new Cases();
   $aCase = $c->loadCase($newCaseId);
   $aCase['APP_STATUS'] = 'TO_DO';
   $c->updateCase($newCaseId, $aCase);
};

If using ProcessMaker 3.2 and later, an optional parameter has been added to set the status of new cases created with the PMFNewCase() function, so it is no longer necessary to manually change the status of cases.

Nonetheless, if needing to manually change the status of cases in version 3.2 and later, the Cases::updateCase() and ListParticipatedLast::update() methods can be used to manually change the status of cases, as shown in this example:

if ($newCaseId) {
   $c = new Cases();
   $aCase = $c->loadCase($newCaseId, 1);
   $aCase['APP_STATUS'] = 'TO_DO';
   $c->updateCase($newCaseId, $aCase);
   //change the status of the case in the Home > Participated list:
   $aData = array(
      'APP_UID' => $aCase['APP_STATUS'];
      'USR_UID' => $aCase['CURRENT_USER_UID'];
      'DEL_INDEX' => $aCase['DEL_INDEX'],
      'APP_STATUS' => 'TO_DO';
   );
   $oListPartLast = new ListParticipatedLast();
   $oListPartLast->update($aData);
}

Note that the above method should only be used if setting the status of a case to "TO_DO" or "DRAFT". if needing to change the case's status to "CANCELLED", "DELETED" or "PAUSED", then the corresponding PMFCancelCase(), cases::removeCase() or PMFPauseCase() functions should be used. To change the status to "COMPLETED", the PMFDerivateCase() function can be used to complete a final task in the process.

Create new case with a self service task

When a new case is started with the PMFNewCase() or PMFNewCaseImpersonate() functions (or the corresponding web services or REST endpoints), the case will be assigned to the specified user. This is a problem if the case needs to start with a task which has Self Service or Self Service Value Based Assignment, so it should not have an assigned user.

The workaround to this problem is to create a dummy task before the self service task in the process:

Then, after creating the new case, call the PMFDerivateCase() function, the routeCase() web service or the PUT /cases/{app_uid}/route-case REST endpoint to route the new case from the dummy task to the self service task. The case should then appear in the Home > Unassigned list, where any of the users who are in the assignment pool for the self service task can claim it.

Example with a Self Service task:

The following trigger code uses PMFNewCase() to first create the case and then PMFDerivateCase() to route to the self service case which is the second task in the process. The unique IDs of the process and the initial dummy task are found by running a case and looking at the values of the @@PROCESS and @@TASK system variable in the Debugger.

$processId = '1822559215775c714dadba1052021519'; //set to the Process ID
$dummyTaskId = '3609934335775a15491fb93026385324'; //set to ID of the dummy task
@@newCaseId = PMFNewCase($processId, @@USER_LOGGED, $dummyTaskId, array());
PMFDerivateCase(@@newCaseId, 1);

This code will work as long as the logged-in user is also in the assignment pool for the initial dummy task. If not, then use the ID of a user who is in that assignment pool.

With a Self Service Value Based Assignment task

If a new case should begin with a Self Service Value Based Assignment task, then the variable which holds the assignee(s) to the task needs to be passed to the new case.

In this example, there is one process with the "Start case" task:

and another process with a "dummy task" and a "Self service task" which uses Self Service Value Based Assignment:

The "Start case" task contains a DynaForm with a dropdown box associated with the @@selfServiceAssignee variable. This dropdown is used to select the user or group to assign to the Self Service Value Based Assignment task:

In the properties of this dropdown box, the datasource is set to "array variable" and the data variable is set to @=aAvailableAssignees. This variable holds a array of the users and groups assigned to the "Self service task".

The following trigger code, which is fired before the DynaForm, populates the list of available users and groups in the dropdown box:

require_once "classes/model/Groupwf.php";
$g = new Groupwf();

//set to ID of self service value based assignment task
$selfServiceTaskId = '4776332015775a154c5b053070364112';
//pool of users/groups that can be selected for the self service task
@=aAvailableAssignees = array();

//look up users/groups assigned to the self service task
$query = "SELECT USR_UID, TU_RELATION FROM TASK_USER WHERE TAS_UID='$selfServiceTaskId'";
$aAssignees = executeQuery($query);

foreach ($aAssignees as $aAssignee) {
   //if a group, then look up group info:
   if ($aAssignee['TU_RELATION'] == 2) {
      $aInfo = $g->Load($aAssignee['USR_UID']);
      if ($aInfo['GRP_STATUS'] != 'ACTIVE')
         continue;
               
      @=aAvailableAssignees[] = array($aAssignee['USR_UID'], $aInfo['GRP_TITLE']);
   }
   else { //if a user
      $aUserInfo = userInfo($aAssignee['USR_UID']);
      if ($aUserInfo['status'] != 'ACTIVE')
         continue;
               
      $fullName = $aUserInfo['firstname'].' '.$aUserInfo['lastname'].'
       ('
.$aUserInfo['username'].')';
      @=aAvailableAssignees[] = array($aAssignee['USR_UID'], $fullName);
   }
}

The trigger code retrieves the list of assigned users/groups from the USER_GROUP table in the database and places them in the @=aAvailableAssignees array to display in the dropdown. If the user or group's status is not set to 'ACTIVE', then it is not included in the list.

After the DynaForm, a second trigger is executed to create the new case:

if (isset(@@selfServiceAssignee) and !empty(@@selfServiceAssignee)) {
   $processId = '3711601605775a272301e35039931543'; //set to the Process ID
   $dummyTaskId = '3609934335775a15491fb93026385324'; //set to ID of dummy task
   @@newCaseId = PMFNewCase($processId, @@USER_LOGGED, $dummyTaskId,
      array('selfServiceAssignee' => @=selfServiceAssignee));

   if (empty(@@newCaseId)) {
      $g = new G();
      $g->SendMessageText("Error creating case: " . isset(@@__ERROR__) ? @@__ERROR__ : '.
      '
, "ERROR");
   }
   else {      
      PMFDerivateCase(@@newCaseId, 1);
      $c = new Cases();
      $aCaseInfo = $c->LoadCase(@@newCaseId, 1);
      $caseNo = $aCaseInfo['APP_NUMBER'];
      $ng = new Groups();
               
      if ($ng->verifyGroup(@@selfServiceAssignee)) {   //if a group
         $g = new G();
         $g->SendMessageText("New case #$caseNo assigned to group ".
                "'". @@selfServiceAssignee_label."'.", 'INFO');
      }
      else { //if a user, then claim the case for that user:
         $c->setCatchUser(@@newCaseId, 2, @@selfServiceAssignee);
         $g = new G();
         $g->SendMessageText("New case #$caseNo claimed by user ".
                "'".@@selfServiceAssignee_label."'.", 'INFO');
      }
   }
}

The @@selfServiceAssignee variable is passed to the new case which is created with the PMFNewCase() function, so that the ID of the user/group selected in the dropdown box will assigned to the "Self service task" in the second process.

After creating the new case, the trigger code then calls PMFDerivateCase() to move the new case from the "dummy task" to the "Self service task". If an individual user was assigned to the task, then the trigger calls Cases::setCatchUser() to claim the case for that user. That way the user does not have to look under HOME > Unassigned to find the case and manually claim the case.

Finally, G::SendMessageText() is called to display a message at the top of the next screen to inform the user of the case number of the new case and who the case was assigned to.

In order for this example to work, set the "Self service task" to use the @@selfServiceAssignee variable in its "Assignment Rules" properties:

Make sure that all the users/groups which are assigned to the "Start Case" task in the first process are also assigned to the "dummy task" in the second task. This is necessary, because the logged-in user needs to have the rights to call PMFDerivateCase() and route paste the "dummy task".

To test this example, download and import the Start Case with self service process (right click and select "Save As").

Select the user for a Value Based Assignment task

Value Based Assignment can be used if needing to select a user to assign to task. This example shows how to populate a dropdown box in a DynaForm to select the user to assign to a task with Value Based Assignment. It also shows how to assign a user randomly from the pool of available users for a task.

The following process is used to complete a work order. The second task named "Do Work" uses Value Based Assignment. On the first pass through the "Do work" task, the user is chosen randomly from the pool of available users assigned to the task. In the third task named "Inspect work," a manager decides whether the work was done properly. If not, the manager can mark a checkbox to redo the "Do Work" task and can select a user from a dropdown box to assign to the task. Then the process will loop back to the "Do Work" task.

To implement this process, first create the following variables:

  • A string variable named "userAssignedToWork" which will be the variable used for Value Based Assignment in the "Do Work" task.
  • An array variable named "availableUsers" which will hold the list of available users in the assignment pool for the "Do Work" task.
  • A boolean variable named "doWorkOver" for a checkbox which is marked to redo the "Do Work" task.

Second, right click on the "Do Work" task, and select Assignment Rules from its context menu. In the dialog box that opens, select Value Based Assignment and set its Variable for Value Based Assignment to @@userAssignedToWork.

Third, create the following DynaForm which the manager in the "Inspect Work" task will fill out if the "Do Work" task was not completed correctly in order to redo the task.

In this DynaForm, the "Do work over?" checkbox is associated with the "doWorkOver" variable. The "Assign work to" dropdown box is associated with the "userAssignedToWork" variable. In the dropdown's properties, the datasource is set to "array variable" and the data variable is @@availableUsers. Set this DynaForm as a step in the "Inspect Work" task.

Fourth, go to the process map and right click on the exclusive gateway and select Properties from its context menu. In its routing rules, set the following conditions:

If the "Do work over?" checkbox is marked, its @@doWorkOver_label variable will be set to "true"; otherwise it will be set to "false". In routing rules conditions, is recommended to use isset() to check whether the variable exists, just in case the user decided to use the Steps menu to skip the DynaForm, so the variable was not saved.

Fifth, create the following trigger to randomly assign a user to the "Do Work" task from the pool of available users:

//use the debugger to find the ID of the task:
$nextTaskId = '188586462577ed40a325599037697166';
$d = new Derivation();
$aUsers = $d->getAllUsersFromAnyTask($nextTaskId);
$cnt = count($aUsers);
if ($cnt == 0) {
   die("Please assign users to the "Do Work" task.");
}
@@userAssignedToWork = $aUsers[rand(0, $cnt - 1)];

The Derivation::getAllUsersFromAnyTask() method is called to get an array of the unique IDs of the users assigned to the "Do Work" task. Then, the rand() function is used to randomly select one of those users to assign to the "Do Work" task. Set this trigger to execute before assignment in the "Fill work order" task.

Sixth, create another trigger to populate the list of users in the "Assign task to" dropdown box:

//use the debugger to find the ID of the task:
$nextTaskId = '188586462577ed40a325599037697166';
@@availableUsers = array();
$d = new Derivation();
$aUsers = $d->getAllUsersFromAnyTask($nextTaskId);
foreach ($aUsers as $userId) {
   $aUserInfo = userInfo($userId);
   $fullName = $aUserInfo['firstname'].' '.$aUserInfo['lastname'].'
    ('
.$aUserInfo['username'].')';
   @=availableUsers[] = array($userId, $fullName);
}

This trigger calls Derivation::getAllUsersFromAnyTask() to get an array of the users and then loops through the list, copying the user's IDs and full name into the @=availableUsers array, which will populate the list in the dropdown box. Set this trigger to execute before the DynaForm in the "Inspect Work" task.

Finally, assign users to the different tasks in the process, so that cases can be run in the process.

To test this example, download and import the Loop Around process (right click and select "Save As").

Pausing Cases between Activities

The intermediate timer event is designed to pause a case between activities (tasks and subprocesses) in the process, but the date and time for this event is fixed and cannot be set dynamically. Instead, the PMFPauseCase() or Cases::pauseCase() functions can be used to pause a case. However, these functions immediately pause the case, rather than pausing them between activities. This trigger example shows how to call PMFPauseCase() between activities and dynamically set the datetime when the case will unpause.

In the following process, the case is paused between "Task 1" and "Task 2". In Task 1, there is a DynaForm where the user selects a datetime when the case should start "Task 2".

First, create two variables:

  • A datetime variable named "pauseTillDate", which will set the datetime when the case will unpause.
  • A datetime variable named "todayDate", which will set the minimum date in the date picker.

Second, create the following DynaForm with a datetime field associated with the "pauseTillDate" variable. In the datetime's properties, set its min date to @@todayDate. If needing to set the hours and minutes when the case will unpause, set its format to YYYY-MM-DD HH:mm.

Set this DynaForm as a step inside "Task 1".

Third, create the following trigger, which will set @@todayDate to today's date:

@@todayDate = getCurrentDate();

Set this trigger to execute before the DynaForm containing the datetime field.

Fourth, create another trigger which will call PMFPauseCase(). It uses Cases::LoadCase() to look up the user assigned to the next task in the process.

//get the user assigned to the next task:
$c = new Cases();
$aCaseInfo = $c->LoadCase(@@APPLICATION, @%INDEX+1);
//if next task is a subprocess then no assigned user:
if (empty($aCaseInfo['CURRENT_USER_UID']))
    $userId = @@USER_LOGGED;
else
    $userId = $aCaseInfo['CURRENT_USER_UID'];

PMFPauseCase(@@APPLICATION, @%INDEX+1, $userId, @@pauseTillDate);

Set this trigger to execute after routing in "Task 1". At that point, "Task 2" will have already been created in the database, so that the case can be paused in the second task.

The steps and triggers in "Task 1" should have this placement:

Make sure to configure the server where ProcessMaker is installed to periodically execute the cron.php script, so that cases will be unpaused at the configured datetime. When a case is run, it will pause until the selected datetime before starting "Task 2". If the user does not select a datetime to pause the case, then it will pause indefinitely until the user assigned to "Task 2" manually unpauses the case.

To test this example, download and import the Pause Case process (right click and select "Save As").

Sending an email when a case is unpaused

The following example shows how to send an email to the assigned user of the current task when a case is unpaused. First, create a separate process that contains a script task which will be periodically executed.

Note that it is not possible to start a process with a script task, so the "dummy task" has to be created to start the process.

Right click on the intermediate timer event, and select Properties from its context menu. In the "Timer Event Properties" dialog box that appears, select the Wait for option and set the Minutes to "20".

Then, create the following email template named 'unpauseCase.html' which will be sent when the case is unpaused:

Case #@=caseNo has been unpaused so that you can work on it.

Case Title: @=caseTitle
Paused: @=casePaused
Unpaused: @=caseUnpaused
Task: @=currentTask
Due Date: @=taskDueDate
Current User: @=taskAssignedUser

Then, create the following trigger in the same process:

$now = date("Y-m-d H:i:s");
$sql = "SELECT * FROM APP_DELAY WHERE APP_DISABLE_ACTION_USER='0' AND ".
       "APP_DISABLE_ACTION_DATE>='$now' AND APP_TYPE='PAUSE' ";
$aCases = executeQuery($sql);

foreach ($aCases as $aCase) {
   $c = new Cases();
   $c->unpauseCase(
       $aCase['APP_UID'],
       $aCase['APP_DEL_INDEX'],
       $aCase['APP_DELEGATION_USER']
   );
   //add 1 because unpauseCase increments index
   $aCaseInfo = $c->LoadCase($aCase['APP_UID'], $aCase['APP_DEL_INDEX']+1);
       
   $t = new Task();
   $aTask = $t->Load($aCaseInfo["TAS_UID"]);

   $aUser = userInfo($aCase['APP_DELEGATION_USER']);   
   $caseNo = $aCaseInfo["APP_NUMBER"];
   $aVars = array(
           "caseNo"          => $caseNo,
           "caseTitle"       => $aCaseInfo['APP_TITLE'],
           "casePaused"      => $aCase['APP_ENABLE_ACTION_DATE'],
           "caseUnpaused"    => $now,
           "currentTask"     => $aTask['TAS_TITLE'],
           "taskDueDate"     => $aCaseInfo['DEL_TASK_DUE_DATE'],
           "taskAssignedUser"=> $aCaseInfo['CURRENT_USER']
   );
   @@ret = PMFSendMessage(
      @@APPLICATION,
      'admin@example.com',
      $aUser['mail'],
      '',
      '',
      'Case '.$caseNo.' unpaused',
      'unpauseCase.html',
      $aVars
   );  
}

Change 'admin@example.com' to match the email of the default account set under Admin > Settings > Email Servers.

This trigger uses executeQuery() to look up the cases which are ready to be unpaused in the APP_DELAY table in the database. For each case, the trigger uses the Cases::unpauseCase() method to pause the case. When the case is paused, its delegation index is incremented by 1. Then, details about the case are obtained with Cases::LoadCase(), Task::Load() and userInfo(). These details are stored in an array of variables that will be passed to the PMFSendMessage() function, which sends an email to the user assigned to the case.

Set this trigger to be executed by the script task. Right click on the script task and select Properties from its context menu. Then, select this trigger in the dropdown box. Then, assign a user to the "dummy task" and run a case in the process.

Finally, configure the ProcessMaker server to periodically execute the timereventcron.php script as a cron job in Linux/UNIX or a Scheduled Task in Windows. Make sure that timereventcron.php is executed before cron.php, because cron.php will also unpause cases so there won't be any cases to unpause by the above trigger if it is run after cron.php.

To test this process, download and install the Periodic_Script_Task-1.pmx process (right click and select "Save Link As").