Contents: [hide]

Overview

A trigger is a piece of PHP code that can be inserted into a process. Triggers provide the appropriate framework to perform complex calculations and add additional functionality to processes.

A ProcessMaker trigger is parsed like a PHP script, so normal PHP variables and functions can be used inside triggers, as well as ProcessMaker's case variables and system variables. PHP libraries can also be imported into ProcessMaker triggers with the require_once() function. With access to the PHP language and its libraries, a great deal of functionality can be added to a trigger.

Trigger Timing

Triggers can be fired at many points during the process:

  1. Before a step is executed (i.e., DynaForms, Input Documents and Output Documents).
  2. After a step is executed.
  3. Before a user is assigned (designated) to work on the next task is the process.
  4. Before a case is routed to the next task in the process.
  5. After a case is routed to the next task in the process.

For example, a process whose first task contains a DynaForm step and an Input Document step would have triggers fired in the following order:

Before step trigger
DynaForm step
After step trigger

 

Before step trigger
Input Document step
After step trigger

 

Before assignment trigger
Assignment of user for next task

 

Before routing trigger
Routing to next task
After routing trigger

 

When to set Triggers to Fire

Before a step
If the step is a DynaForm, this is a good point to set any case variables which will be displayed in the DynaForm's fields. If the step is an Output Document, then case variables can be set to be used in the Output Document's template.
After a step

After a DynaForm step, the data from the DynaForm's fields exists as case variables, but it hasn't yet been submitted to the database and any files uploaded to File fields have not yet been saved to the file system. This a good point to alter the data before it gets submitted.

Warning: If the die(), exit() or some other function is called which stops the completion of the trigger, it will prevent the data and files in the DynaForm from being saved to the database. If needing to use die() or exit() in a trigger, set the trigger to fire before the next step (or before assignment if the last step in the task). Alternatively, call PMFSendVariables(@@APPLICATION, ...) to save the variables in the current case before calling die() or exit().

A trigger set to fire after an Input Document step is fired every time another file is added to the Input Document, so it may be fired multiple times. The trigger is fired after the new file has already been added to the database and saved in the server's file system, so it isn't possible to prevent the file from being saved with trigger code, but it is possible to look up the latest file which was just added to the APP_DOCUMENT table and call AppDocument::remove() to eliminate the latest file from the Input Document.

If no file is uploaded to an Input Document, then a trigger set to be fired after the Input Document will not be fired. If needing the trigger to always fire, then set the trigger to fire before the next step in the task (or before assignment if the last step in the task).

Before Assignment
If the next task in the process uses Value Based Assignment, this is a good point to set the variable used to determine the next user. Note that before assignment triggers will always fire, even in final tasks when there is no user assignment for the next task.
Before Routing (derivation)
This is a good point to set any variables used by conditions, to direct the flow to the next activity (task or sub-process).
After Routing (derivation)
At this point, the record for the next task has already been written to the database in the wf_<WORKSPACE>.APP_DELEGATION table, so it can be consulted with database queries. Before this point, the next task and the next user can not be known for sure.

Case Variables

In addition to normal PHP variables, triggers can use case variables, which are variables which hold information about a ProcessMaker case and are only valid while that case is running. These variables are defined under the list of "Variables" in the Process Map. Controls in DynaForms, such as textboxes, dropdown boxes, grids, etc., can be associated to a case variable to hold the values entered into that control. When a user clicks a submit button in a DynaForm, the value in the control is saved to its case variable and is automatically created for each control with the entered data as its value.

Not only can triggers access case variables defined in the "Variables" list, they can also define additional case variables. If a subsequent DynaForm in the case uses the variable in a field, the field's value will automatically be set to the value of the variable. When the DynaForm is submitted, the value in the field will be assigned to its case variable.

For instance, a trigger could define the case variable @@ApplicationDate and store the current date in it. A subsequent DynaForm containing the field whose variable is "ApplicationDate" would display that date. The user could alter the date in the DynaForm, which would then change the value stored in the case variable @@ApplicationDate. A subsequent trigger could examine @@ApplicationDate and make calculations based on that date.

Case variables are defined like normal PHP variables, except they use @@, @%, @#, @?, @$ and @= instead of $ to identify them. For example:

@@myVariable = "Hello world!";

Where:

@@myVariableA case variable parsed as a string.
=The equals symbol means that the entire right side of the expression will be assigned to a variable.
"Hello word!"A string of characters to assign to the variable. All strings need to either be enclosed in '(single quotation marks) or " (double quotation marks). In contrast, integers such as 987 and floating-point numbers such as 19.87 are not enclosed in quotation marks.
;A semicolon indicates the end of a trigger statement. Each trigger statement should be finished with a semicolon.

Note that case variables only have scope when their case is open. To create global variables which can be accessed by any case, use this workaround.

Typing rules for Case Variables

When referencing a case variable within a trigger, take into account the following typing rules:

  • @@variable_name
    The variable will be parsed as string type. If originally another type, it will be converted to a string. So -1, 0 and 12.45 become "-1", "0" and "12.45", respectively.
  • @%variable_name    
    The variable will be parsed as integer (a whole number). If originally another type, it will be converted to an integer. A real number will be converted to a whole number, so 12.99 becomes 12. The decimal portion of the number is dropped and there is no rounding up in the conversion. A string which contains numbers will be converted to a whole number, so "655" and "34.19" become 655 and 34, respectively.
  • @#variable_name
    The variable will be parsed as a float type (a real number with decimals).
  • @?variable_name    
    The variable will be parsed as a string used in a URL. The string is transformed with PHP's encodeurl(), so all spaces become pluses (+) and all characters which aren't letters, except underscores (_), are replaced by a % followed by a 2 hexidecimal digits, so that the string can be used in posted data to/from WWW forms.
  • @$variable_name
    The variable will be parsed as a string for use in an SQL query. Any single quotation marks (') will be preceded by a backslash. So "I'm telling you it's a boy!" becomes "I\'m telling you it\'s a boy!".
  • @=variable_name    
    The variable's type is not be changed and it is parsed as its original type. Always use this when referencing objects and arrays.

For example, this statement uses 3 case variables which are parsed as a string, an integer and its original type, respectively.

@@other_value = @%any_number + @=age - 10;

Where:

  • any_number could be a number from a text field in a DynaForm; this will be parsed like an integer value.
  • age could be a field in any DynaForm, but it's type will not be transformed.

Type conversion of case variables only happens when the variable is an R-value, which means that it is what is on the right-hand side of an assignment statement and is what is assigned from:

L-value (assigned to) = R-value (assigned from);

If a case variable is a L-value or is what is assigned to, no type conversion happens. For example:

$x  = @#y;    //R-value, so transformed to float before assignment
$a  = @@b;    //R-value, so transformed to string before assignment
@#c = "40.0"; //@#c is a L-value, so assigned a string and no type conversion
@@d = 40.0;   //@#d is a L-value, so assigned a float and no type conversion

It is important to note that variable_name follows the PHP rules for variable names definition. That means a valid case variable name in ProcessMaker starts with a letter or underscore, followed by any number of letters, numbers, or underscores. Case variables are case sensitive, so @@myvar, @@MyVar and @@MYVAR are three different variables. If passing values between triggers and DynaForms, make sure that the case variable name in the trigger is spelled the same as the variable name used in a DynaForm field. ProcessMaker case variables are stored separately from normal PHP variables, so a trigger with $myVar and @@myVar would have two different variables.

ProcessMaker retains the original value of case variables as normal PHP variables, so they not altered by using the different conversion rules. For instance:

@=MyVar = 22.345;
$sum = 5 + @%MyVar;       # Use MyVar as the integer 22
$str = "Sum: " . @@MyVar; # Use MyVar as the string "22.345"
                         # @=MyVar still equals its original value of 22.345
$total = @=MyVar + 1;     # $total equals 23.345

ProcessMaker case variables can not be inserted into strings like normal PHP variables. The following statement will not work:

$str = "My name is @@MyName and my age is @%MyAge.\n";

To insert case variables in a string, either reassign them to normal PHP variables and insert them:

$my_name = @@MyName;
$my_age = @%MyAge;
$str = "My name is $my_name and my age is $my_age.\n";

Or use the dot operator (.) to concatenate strings with case variables:

$str = "My name is " . @@MyName . " and my age is " . @%MyAge . ".\n";

When a case variable which an integer or floating point number is concatenated with . (dot operator), it is automatically converted to a string.

How to use case variables

Case variables hold data which is specific to a case and they can either be set in a DynaForm field or a trigger. Unlike system variables which are defined automatically by ProcessMaker, case variables are defined in the "Variables" list inside the Process Designer or by defining them in triggers. Both types of variables can be used in:

For instance, inside the properties of an activity, for the Variable for Case priority field:

Click on the "@@" button to access the list of case and system variables.

    • Prefix. Select the prefix the variable will use in the field. These prefixes are explained in this section.
    • Help icon. Hover the mouse pointer over the "?" icon to view the existing prefixes for variables:

  • Text to search. Enter the name of the variable to be rapidly found.
  • Variable. This column lists the names of all existing variables. If needed to introduce any of them, select it and click on the "Insert Variable" button at the bottom of the window.
  • Label. This column indicates whether it is a system or case variable.
  • Pagination control. Each page lists only ten items, navigate through all existing variables with this control.
  • Insert variable. Click on this button to insert a variable.

System Variables in Triggers

In addition to case variables, triggers can also use system variables, which are predefined variables which have the same syntax as case variables and hold information about the current ProcessMaker system.

ProcessMaker has the following system variables:

Variable NameVariable Description
@@SYS_LANGCurrent system language in two letter ISO 639-1 code, which by default is "en" (English).
@@SYS_SKINCurrent system skin, which by default is "neoclassic" in ProcessMaker 3.0.x.
@@SYS_SYSCurrent workspace name, which by default is "workflow".
@@PROCESSCurrent process UID.
@@TASKCurrent task UID.
@@APPLICATIONCurrent case UID.
@@APP_NUMBERCurrent case number. Available Version: 3.0.1.8 and later.
@@USER_LOGGEDUID of the current user.
@@USR_USERNAME Username of the current user.
@%INDEXThe delegation index number, which is a positive integer which counts tasks in a process, starting from 1. If multiple tasks are operating in parallel, then each task will have a different index number.
@@PINThe 4 character PIN, which can be used to access information about the case without being a registered user at:
  http://<IP-ADDRESS>:<PORT>/sys<WORKSPACE>/<LANG>/<SKIN>/tracker/login
@@__ERROR__If a ProcessMaker error occurs, this system variable will be created, containing the error message. Note that this system variable only exists after a ProcessMaker exception occurs. It will not be created by syntax errors in PHP or JavaScript or by errors which ProcessMaker doesn't know how to handle.

 

Note: UID stands for "unique identification" which is a string of 32 hexadecimal characters to identify objects in ProcessMaker. To find the UIDs for processes, tasks, cases, users, groups, etc., see Consulting the ProcessMaker databases.

Operators

Operators in ProcessMaker triggers are handled like the PHP language.

@#a + @#b   Addition
@#a - @#b   Subtraction
@#a * @#b   Multiplication
@#a / @#b   Division
@#a % @#b   Modulus (Remainder)
@@a . @@b   String concatenation

Terminating Statements

A statement in a ProcessMaker trigger can extend for multiple lines. All statements should terminate with a ; (semicolon) to indicate where the statement ends.

$db = "71525495056005261835755079295218"; //Unique ID of the database connection
$grid = @=grid;
$rows = count($grid);
for ($i=1; $i <= $rows; $i++) {
   $quantity = $grid[$i]["Quantity"];
   $description = addslashes($grid[$i]["Description"]);
   $unitCost = $grid[$i]["UnitCost"];
   $query = "INSERT INTO INVOICE_REPORTS (
         QUANTITY,
         DESCRIPTION,
         UNIT_PRICE
      )
      VALUES (
         $quantity,
         '$description',
         $price
      )"
;
   executeQuery($query, $db);
}

Examples

  • Trigger 1: Get the information of the logged user and assign these values to the @@issueOwner case variable.
@=aData = userInfo(@@USER_LOGGED);
@@issueOwner = @=aData['firstname'] . ' ' . @=aData['lastname'];
  • Trigger 2: Get the current date and time and assign the values to the @@issueDate case variable.
@@aDate = getCurrentDate();
@@aTime = getCurrentTime();
@@issueDate = @@aDate . " " . @@aTime;
  • Trigger 3: Execute a SQL and assign the values to cases variables @@issueDesc1 and @@issueDesc2.
@=Data = executeQuery('SELECT ITEM, ITEM2 FROM PMT_DESCRIPTION');
if (is_array(@=Data) and count(@=Data) > 0) {
   @@issueDesc1 = @=Data[1]['ITEM'];
   @@issueDesc2 = @=Data[1]['ITEM2'];
}

Creating Triggers

To following options are available to create triggers in a project:

  • Create a Custom Trigger: This option allows process designers to create custom triggers in PHP.
  • Copy/Import Triggers from a process: This option allows process designers to copy and import a trigger from a different or the same process within the same workspace.
  • Creating a Trigger with the Wizard: This option allows process designers to create a trigger using the Trigger Wizard, in which it is possible to create triggers based on the functions of ProcessMaker or any other third-party integration.

Custom Trigger

To create a new custom trigger go to the main toolbox of the Process Map and hover the mouse pointer over the "+" icon of the Triggers option. Click  on the option Create that displays to the left.

A new window will open to create a new custom trigger in PHP, as shown in the following image:

Where:

  • Title: Required Field. Give the trigger a title.
  • Description: Give the trigger a description of what it is about.
  • Code: Add the PHP code for the trigger. To make the code easier to read and eliminate careless errors, the code editor contains syntax highlighting, parentheses, bracket matching and line numbering.
  • @@ Button: Click this button to insert either a system or case variable in the trigger code.
  • Open Editor: It opens the editor in a larger window to edit triggers with longer code. After editing the code, click on Apply to save the changes or they will be lost.
  • Cancel: It discards any changes in the trigger and closes the window.
  • Save: It saves the changes made during the creation or edition of a trigger.

Triggers Management

To see the complete list of Triggers created and other options for triggers, click on the Triggers option of the main toolbox of the Process Map:

The window that is opened shows the list of triggers already created in the project:

Where:

1. Text to search: Filters the name of the trigger to be searched.

2. Create: Click on this option to create a new custom trigger. This option is explained in this section.

3. Copy: Click on this option to copy/import a trigger from a process. This option is explained in this section.

4. Wizard: Click on this option to create a new trigger based on the functions of ProcessMaker or any other third-party integration. This option is explained in this section

5. Show ID: Click on this option to obtain the Unique ID of the trigger, which is a string of 32 hexadecimal characters to uniquely identify the trigger.

6. Title: Trigger title. Click on the down arrow to sort the list in descending order. And sort the list in ascending order by clicking in the up arrow.

7. Type: It shows type of trigger created. Read this section to see what kind of triggers can be created.

8. Edit: Click on this option to go into the Trigger editor.

9. Delete: Click on this button to delete the trigger. A message like the following is shown:

It is not possible to delete a trigger which is still assigned as a step in a task. Instead, remove the trigger as a step, before deleting the trigger. If the trigger is still assigned as a step, the following warning will be displayed:

10. Pagination control: Use this control to navigate through the pages, which show 10 files per page.

Note: It is recommended not to change the structure of the process while there are still cases running.

Copy/Import Triggers from a process

To reuse a trigger from a different or the same process within the same workspace, open the process in which the trigger will be used. In the main toolbox, click on the Triggers option. In the window that opens, click on the Copy option:

A new window will open with the options to copy a trigger from a process:

Where:

  • Process: Required field. Select the process from which the trigger will be copied/imported.
  • Trigger: Required field. Select the trigger which will be copied/imported from the process previously selected.
  • Title of the new Trigger: Required field. Give the trigger a title for the current process.
  • Description of the new Trigger: Give the trigger a description.
  • Code: Once the trigger is imported, its code will be included in this section. Edit it only if it is necessary.
  • Cancel: This option cancels the action to import the trigger.
  • Copy Trigger: Click on this option to copy and import the trigger.

Creating a Trigger with the Wizard

The "Wizard" option creates a new trigger using the Trigger Wizard of ProcessMaker.This option is designed to help process designers who don't have great knowledge in the PHP syntax, but still want to be able to create triggers which call a single ProcessMaker function or some other functions available. For each function, the Wizard displays a window to input each parameter for the function and enter a variable to store the return value from the function.

To create a trigger whith Trigger Wizard, open the process in which the trigger will be used. In the main toolbox, click on the Triggers option. In the window that opens, click on the Wizard option:

The window that is opened contains the categories available to create triggers using the Trigger Wizard.

Select the type of function that will be called by the new trigger, such as ProcessMaker Functions or Sugar CRM Functions, etc. Click on the right arrow to expand the list of each one of them. Then, select the specific function to use from the list.

ProcessMaker Functions

ProcessMaker has made a number of predefined PHP functions available be used in triggers and conditions. Click on the down arrow next to "ProcessMaker functions" to view the list of functions:

For example, if it is required to get the list of cases, select the function PMF Case List and then the following windows is opened:

    • Type: It indicates the type of function created. Possible types: Custom, ProcessMaker Functions, Sugar CRM Triggers, Talend ETL Integration, Alfresco DM Triggers v. 0.1, Sharepoint DWS Triggers v. 0.1 and Zimbra Triggers v. 0.1.
    • Title: Required Field. Give the trigger a title. The more descriptive the title is, the easier to work with it later.
    • Description: Enter a brief description about the trigger.
    • Parameters: Enter the parameters for the specific function that will be called. For each parameter, enter a value which can be:
      • A case variable or a system variable.
      • A literal value, such as 644 (an integer), -2999.85 (a floating point number), "Finance Department" (a string), or "447CEAF7BE6AEB747CEB6EB5526B" (a ProcessMaker unique ID). If the parameter type is a string, ProcessMaker will automatically enclose it in double quotation marks if it doesn't start with @. If the string contains a double quotation mark ("), escape it with a backslash. For example: "Say \"hello\""
        If the parameter type is an integer or floating­ point number, ProcessMaker will automatically convert it to a number if it doesn't start with @, so a string value such as "client1" gets converted to 0.
    • Help icon (Parameters): Hover the mouse pointer to see the description of the parameter:
    • Insert Variable button: Use the @@ button to insert either System or Case variables.
    • Variable to Hold the Return Value: Required Field. Some functions have a return value. It is recommended to assign the return value to a case variable, such as @@ReturnVal, so that this value can later be examined in the Process Debugger if errors occur. It doesn't matter what type cast is used in the case variable, because ProcessMaker ignores the type cast when assigning values to case variables, so @%ReturnVal is the same as @@ReturnVal.
    • Help icon (Return Value): Hover the mouse pointer to see a brief the description of the return value.
  • Cancel: It discards any changes and cancels the creation of the new trigger by closing the current modal window.
  • Save: This option saves the configuration. When the trigger is created, the page will be redirected to the main list of triggers and a flash message will appear indicating that the trigger has been saved.
Sugar CRM Triggers

SugarCRM is a web-based customer relations management (CRM) tool that facilitates the use of customer support, marketing and sales business roles by bringing them together under a unified software system. This allows customer relations specialists to quickly and efficiently deal with a wide range of customer demands.

Although SugarCRM can be accessed through a website, its functionalities have been integrated with ProcessMaker, meaning users can manage SugarCRM through web services. By clicking on "Sugar CRM Triggers" the available triggers will display:

Read this documentation for a detailed description of each trigger.

Alfresco DM Triggers v. 0.1

Alfresco is an open source Enterprise Content Management (ECM) system, for Microsoft Windows and Unix-like operating systems. The Alfresco functions allow ProcessMaker triggers to manage documents and folders in Alfresco. By clicking on "Alfresco DM Triggers v. 0.1" the available triggers will display:

Read this documentation for a detailed description of each trigger.

Sharepoint DWS Triggers v. 0.1

Microsoft Office SharePoint Server includes document management features that you can use to control the life cycles of documents in your organization — how they are created, reviewed, published, secured, and consumed, and how they are ultimately disposed of or retained. By clicking on "Sharepoint DWS Triggers v. 0.1" the available triggers will display:

Read this documentation for a detailed description of each trigger.

Zimbra Triggers v. 0.1

Zimbra integration allows to navigate through ProcessMaker by using Zimbra application. Moreover, it allows users to create folders, contacts and appointments using triggers. By clicking on "Zimbra v Triggers. 0.1" the available triggers will display:

Read this documentation for a detailed description of each trigger.

Inserting a Trigger in a Process

After creating a trigger, it can be inserted at various points in a process. Login to ProcessMaker with a user such as "admin" who has the PM_FACTORY permission in his/her role. Then, open a process for editing.

  • In the process map, right click on the task where the trigger will be fired and select the Steps option from the menu:

    SelectStepPropertiesFromMenu.png

  • To select the runtime for a given trigger, go to the "Triggers" section inside the "Available Elements" panel and grab the trigger that will be assigned by its name without releasing the cursor of the mouse. Grab it where the trigger will be executed. Notice that expanding each step, there are two sections: "Before (DynaForm, Input Document, Output Document)" and "After (DynaForm, Input Document, Output Document)". In the section "Assignment", steps can be placed only "Before Assignment", and in the section "Routing", triggers can be assigned "Before Routing" or "After Routing".

    A flash message is shown at the top of the panel indicating that the step was assigned correctly.

  • Drop the element in the gray space enabled and the step will be added:

Firing Triggers Based on Conditions

If only wanting the trigger to fire in certain situations, enter a condition that must evaluate to true for the trigger to be fired. Use the @@ button to insert system and case variables in the condition. If the trigger does not have a condition, it will always be executed.

To add a condition to a trigger, go to the "Triggers" section inside the "Assigned Elements" panel and click the Condition button:

AssignTriggerToStep.png

The window that displays has the @@ button that allows adding existing variables. Enter the condition in the "Condition" field and save the settings by clicking on the Save button:

The "*" mark will appear in the Condition button of the trigger to indicate that its execution is based on a condition. This visual clue reminds process designers which triggers have defined conditions, which is helpful when trying to figure out why a trigger did not fire:

GreenTickConditionsTriggerV2 8.png

Debugging Triggers

When running cases, it is possible to see when triggers are executed and the value of system and case variables by turning on the ProcessMaker Debugger.

Enabling Debug Mode

There are two ways to enable Debug Mode, so the ProcessMaker Debugger will be displayed in a left sidebar while running cases for a particular process.

Login to ProcessMaker with a user such as "admin" who has the PM_FACTORY permission in his/her role. Then, go to the Designer menu. Right click on a process in the list and select the Enable Debug Mode option from the dropdown menu.

EnableDebugModeInProcessList.png

Debug Mode can also be enabled while editing a process. Right click on a blank area in the Process Map and select the option Edit Process from the menu.

SelectPropertiesFromProcessMapMenu.png

In the Properties dialog box, mark the Debug checkbox:

MarkDebugCheckboxInProcessProperties.png

Using the ProcessMaker Debugger

To use the ProcessMaker Debugger, run a case for a process which has enabled Debug Mode. The ProcessMaker Debugger will appear in a left-hand sidebar to view the system and case variables and see the code of the triggers that have just been executed.

The Variables tab shows the values of the system variables and the case variables for the currently open case. Each variable is displayed, along with its value. To see the content of long strings and arrays, click on a variable to see its value displayed in the box below.

DebuggerVariablesTab.png

The list of variables can be filtered by clicking on the buttons at the top:

  • All: Displays all system and case variables for the current case.
  • DynaForm: Displays the case variables for the current case.
  • System: Displays the system variables.

The Triggers tab shows which triggers have just executed and whether they executed before or after the last step or routing. To see a particular trigger's code, click on the trigger and its code will be displayed in the box below.

DebuggerTriggersTab.png

To see a trigger's code in a separate window, click on the Open in a New Window.

DebuggerTriggerCodeWindow.png

After each step, the Debugger will pause the case, so the variables and triggers can be examined in the Debugger. To continue running the case, click on the Continue button.

DebuggerContinueButton.png

The Debugger will also pause after the task has been routed (derived), so the results of the triggers from before assignment, before derivation and after derivation can be examined.

Viewing Error Messages

If a fatal PHP error occurs in the trigger code when running a case, a message will be displayed in red saying "Fatal error in trigger" and the error message will be placed in the @@__ERROR__ system variable. To see the error message, either turn on the debugger or examine the error in the wf_WORKSPACE.APPLICATION.APP_DATA field in the database.

If PHP is configured to stop on errors, then the case will stop and a PHP error message will be displayed in the cases frame:

If a PHP warning or non-fatal error (such as a syntax error) occurs while executing a trigger, the execution of the trigger will stop and a message will be displayed at the top of the cases frame. Nonetheless, the execution of the case will continue, so the use can work on the next step in the task. These are the only types of error messages whose line numbers correspond to the line numbers of the trigger code.

All of these types of errors are immediately obvious to the user running the case, however, ProcessMaker exceptions will not be displayed to the user when running a case. When a ProcessMaker exception occurs, the system variable @@__ERROR__ is created to store its error message.

The easiest way to detect these types of errors is to check if the @@__ERROR__ variable has been defined. Add trigger code which displays the contents of the @@__ERROR__ to the user when running cases. For example, the following code checks if an a ProcessMaker exception has occurred. If so, it stops the case with the die() function and displays the contents of the error message to the user:

if (isset(@__ERRROR__)) {
   die(@@__ERROR__);
}

Examining Variables

It is often difficult to figure out what is the problem with trigger code, because the values of variables can't be seen when running a case except if they are displayed into DynaForm fields. If debugging code, it is recommended to add code to view the value of variables.

For example, the G::SendTextMessage() function can be used to display the contents of a variable at the top of the screen, without stopping the case. to the user, without stopping unable to figure out why a database query is failing, assign the result to a case variable so it can be seen in the Debugger:

@@result = executeQuery("SELECT ...");

If needing even more detailed information about a variable, the contents of variables can be displayed with var_dump() wrapped in <pre> tags:

$result = executeQuery("SELECT ...");
print "<pre>";
var_dump($result);
print "</pre>";

If needing to force trigger execution to stop at a certain point, so variables can be displayed, then call die(). Be aware, however, that die() will stop the execution of the case from continuing onto the next step and will stop DynaForm data from being saved if called after a DynaForm step.

$result = executeQuery("SELECT ...");
print "<pre>";
var_dump($result);
die("</pre>");

Error Management

Error Management for Processes in Production

Activating the ProcessMaker Debugger is a good way to detect errors when designing processes, however, it is not recommended when running cases in production. Not only is the debugger distracting for normal users, it also creates a potential security hole if users can see data entered by other users in the case.

Most of the time, bugs will be detected when designing the process, but some types of bugs are caused by unexpected user input or unanticipated circumstances such as a condition which contains a variable which didn't get defined because a step gets skipped. The most common source of these errors is unexpected user input which causes problems when querying the database in a dependent field or when a trigger calls executeQuery().

If needing to detect errors when a process is in production, it is recommended to add Triggers which check for the existence of the @@__ERROR__ variable, in order to display its message to users. Use the isset() function to detect whether the @@__ERROR__ variable exists and display the message to the user with die() or the G::SendMessageText() method. Then, remove the @@__ERROR__ variable with the unset() function, so it can be set again when the next ProcessMaker exception occurs.

For example:

if (isset(@@__ERROR__)) {
   G::SendMessageText(@@__ERROR__, 'ERROR');
   unset(@@__ERROR__);
}

The @@__ERROR__ variable will be stored as a case variable, so it will stay in the case if it isn't unset. Because it is persistent, it can be checked for several tasks later or at the end of a case.

It might also be a good idea to mail the exception's message to an administrator, so he/she knows that something is going wrong with the process.

For example, if using the following email template named "errorForm.html":

Date: @#date
Time: @#time
Case: @#APPLICATION
Username: @#USR_USERNAME
Process ID: @#PROCESS
Task ID: @#TASK
Error Message: @#errorMessage

Then, the following trigger could mail this form to the system administrator with the PMFSendMessage() function when an exception occurs:

if (isset(@@__ERROR__)) {
   $vars = array('date' => getCurrentDate(), 'time' => getCurrentTime(), 'errorMessage' => @@__ERROR__);
   PMFSendMessage(@@APPLICATION, 'admin@example.com', 'admin@example.com', '', '',
      'ProcessMaker exception occurred', 'errorForm.html', $vars);
   unset(@@__ERROR__);
}

Alternatively, the error messages could be appended to a log file at shared/log/exceptions_log.txt with the file_put_contents() function:

if (isset(@@__ERROR__)) {
   $logFilePath = PATH_DATA . 'log' . PATH_SEP . 'exceptions_log.txt';
   $content = "Datetime: " . getCurrentDate() . ' ' . getCurrentTime() .
      "\nCase: " . @@APPLICATION . "\nUsername: " . @@USR_USERNAME .
      "\nProcess: " . @@PROCESS . "\nTask: " . @@TASK . "\nError: " . @@__ERROR__ . "\n\n";
   file_put_contents($logFilePath, $content, FILE_APPEND);
   unset(@@__ERROR__);
}

Exiting from the Middle of Trigger Code

ProcessMaker does not provide a good way to exit a trigger, except to come to the end of the code. The only way to exit the middle of a trigger is to cause an exception or call the die() function, which would stop the execution of the trigger and prevent the execution of the next step in the process.

In order to write reliable code, it is a good idea to check for unexpected errors and provide a way to exit the trigger. Generally this involves writing long if then else blocks, which quickly become unwieldy if there are very many of them.

For example, a trigger which contains three database queries becomes quite difficult to manage:

$result1 = executeQuery("SELECT ...");
if (is_array($result1) and count($result1) > 0) {
   //some code for query1
   
   $result2 = executeQuery("SELECT ...");
   if (is_array($result2) and count($result2) > 0) {
      //some code for query2
       
      $result3 = executeQuery("SELECT ...");
      if (is_array($result3) and count($result3) > 0) {
         //some code for query 3  
      }
      else {
         die("Error in query3");
      }
   }
   else {
       die"Error in query2");
   }
}
else {
   die("Error in query1");
}

Breaking up the code into functions doesn't help, because calling a function will only temporarily leave the middle of the code. Eventually the function has to return and there is still no easy way to exit. The best way to handle these situations is to use a programming anachronism, the goto statement. It is ugly, but it works.

For example:

$result1 = executeQuery("SELECT ...");
if (!is_array($result1) or count($result1) < 1) {
   $msg = "Error ...";
   goto errorMsg;
}
//some code here
$result2 = executeQuery("SELECT ...");
if (!is_array($result2) or count($result2) < 1) {
   $msg = "Error ...";
   goto errorMsg;
}
//some more code here
$result3 = executeQuery("SELECT ...");
if (!is_array($result3) or count($result3) < 1) {
   $msg = "Error ...";
   goto errorMsg;
}
//some more code here
goto endScript:
 
errorMsg:
   die($msg);
 
endScript:

Variable Storage

Case and system variables for each case are stored in the wf_<WORKSPACE>.APPLICATION.APP_DATA field in the database, which is a MySQL mediumtext field that can hold up to 16MB (224 bytes) of data.

The variables are stored in the APP_DATA field as an associative array which has been converted to a string by PHP's serialize() function. It has the following format:

a:NUMBER-OF-VARIABLES:{ NAME-TYPE1:NUMBER-CHARS-IN-NAME1:VARIABLE-NAME1;VALUE-TYPE1:NUMBER-CHARS-IN-VALUE1:VARIABLE-NAME1; NAME-TYPE2:NUMBER-CHARS-IN-NAME2:VARIABLE-NAME2;VALUE-TYPE2:NUMBER-CHARS-IN-VALUE2:VARIABLE-NAME2; ...}

The NAME-TYPE and VARIABLE-TYPE are almost always listed as type "s", which stands for string. For example:

a:4:{s:7:"repDate";s:10:"2015-10-29";s:7:"repTime";s:8:"10:56:00"; "repUserName";s:9:"Amy Wyron";s:12:"repUserEmail";s:14:"amy@processmaker.com";}

If a DynaForm has a file field, the actual file is stored separately and only the filename (without its path) is stored as a case variable.

The data in the APPLICATION.APP_DATA field is also stored as an associative array within the superglobal $_SESSION variable. In addition to the normal way of accessing case and system variables as @@variable, then can also be accessed as $_SESSION["APP_DATA"]["variable"] for the current case.

To get the system and case variables for any case, the APPLICATION.APP_DATA field can be unserialized into an associative array of variable name and variable value pairs. For example, the following script looks up the current case's variables and outputs them into a string which can be displayed in a DynaForm field named "MyTextArea":

$caseNo = '46'; //lookup the case number for the case in the Case list
$result = executeQuery("SELECT APP_DATA FROM APPLICATION WHERE APP_NUMBER='$caseNo'");
if (is_array($result) and count($result) > 0) {
   $sVars = $result[1]['APP_DATA'];
   $aVars = unserialize($sVars);
   @@MyTextArea = "";
   foreach ($aVars as $varName=>$varValue)
      @@MyTextArea .= "$varName: $varValue\n";
}

The following script does the same thing, using ProcessMaker's Gulliver framework classes:

$CaseNo = '46';      //lookup the case number for the case in the Case list
$result = executeQuery("SELECT APP_UID FROM APPLICATION WHERE APP_NUMBER='$caseNo' ");
$caseUID = $result[1]['APP_UID'];
 
G::LoadClass('case');
$oCase = new Cases();                //Create Cases object
$aCase = $oCase->loadCase($caseUID); //Load a particular case
$aVariables = $aCase['APP_DATA'];    //Get the variables for that case
@@MyTextArea = "";
 
//Loop through the associative array, examining each variable
foreach ($aVariables as $varName => $varValue) {
   @@MyTextArea .= "$varName: $varValue\n";
}

If working with large values in triggers, it is recommended to use normal PHP variables rather than case variables due to their limited storage space. If persistent storage is needed, create a separate database to store the large values and manage the database with executeQuery(). Remember to serialize the variable before saving it to the database, especially if it isn't a string. The PHP language doesn't provide a reliable way to check the memory usage of a variable, but strlen(serialize(@=variable)) can also provide a rough idea of the memory size of a variable in bytes. If worried about exceeding the available memory limit of 16MB, see this example for how to check for memory usage of the APPLICATION.APP_DATA field.

Browser Redirection

Triggers can be used to redirect the web browser to a new page. Use the G::header() function to redirect the web browser to a specified address. It is a wrapper for the standard header() function. Set its parameter to "location:" concatenated with the URL where the web browser should be redirected. For example:

G::header("location: http://www.google.com");
die();

To stop any normal processing after the trigger from occurring, it is a good idea to call the die() function after redirection to immediately kill the trigger.

If redirecting to an address inside ProcessMaker, it not necessary to include the full address. Version 2.0 and later uses multiple frames to display the ProcessMaker interface and cases are run within a child frame. To redirect to the cases list inside the parent frame use:

G::header("location: casesListExtJsRedirector");
die();

The G::header() function only redirects to a new address for the current frame. To redirect the web browser to the topmost frame, trigger code can write JavaScript code inside <script>...</script> tags which redirects the topmost frame. For example, the following trigger redirects the web browser to the Home menu which displays the Inbox:

echo "<script> top.location='../cases/main'; </script>";
die();

Redirecting to a case

When a trigger is executed inside a case, redirection will occur within the "casesSubFrame" iframe, which is the iframe where cases are opened. The G::header() function can be used to open a different case. For example:

G::header("Location: cases_Open?APP_UID=7921628035723c3458afbd5092219945&DEL_INDEX=1&action=draft");
die();

Remember that a case can only be opened if the logged-in user is the assigned user to the current task in the case, has process permissions to open the case (in read-only format) or is a Process Supervisor with rights to open an object in the task specified by the delegation index number. If a user doesn't have rights to open a case, the web browser will redirected back to the cases list.

The case will open in the task specified by the case's unique ID (APP_UID) and the delegation index number (DEL_INDEX). The DEL_INDEX is 1 for the first task in a case, 2 for the second task, etc.

The case will open the first step (DynaForm, Input Document, Output Document or External Step) in the specified task. If needing to open a case and go to the second or a subsequent step at the same time, see Redirecting to a specified task and step below.

Redirecting to a step

Once a case is opened in a specified task, then it is possible to redirect to a specified step (DynaForm, Input Document, Output Document or External Step) within that task. For example, the following trigger redirects to the third step in the current task, which is a DynaForm:

G::header("Location: cases_Step?TYPE=DYNAFORM&UID=8818168195723e0919faaf5044471877&POSITION=3&ACTION=EDIT");
die();

Redirecting to a specified task and step

ProcessMaker does not provide a way to both open a case at a specified task and go to a specified step with that task at the same time. However, it is possible to use JavaScript to work around this problem if the first step in the task is a DynaForm.

In the DynaForm which is the first step in the task, add a hidden field whose name, ID and variable is "redirectUrl". Then, add JavaScript to the same DynaForm which uses Window.location to redirect to the address in the "redirectUrl" field:

var url = $("#redirectUrl").getValue();
if (url != '') {
   location.href = url;
}

Then, create a trigger which calls PMFSendVariables() to set the "redirectUrl" variable in the case and then calls G::header() to redirect to the case:

//variables set dynamically from database searches in the APP_CACHE_VIEW table or
//from case variables set by the user in a previous DynaForm:
$caseId = '4101368135723d3a0e12e75054391524';
$index = 1;
$url = 'cases_Step?TYPE=DYNAFORM&UID=8818168195723e0919faaf5044471877&POSITION=3&ACTION=EDIT';

PMFSendVariables($caseId, array('redirectUrl'=>$url) );
G::header("Location: cases_Open?APP_UID=$caseId&DEL_INDEX=$index&action=draft");
die();

When canceling/pausing/reassigning/opening cases

A trigger can be executed when deleting, canceling, pausing, reassigning or opening cases. To set a trigger to fire during one of these points, open a process for editing and right click on a blank area of the process map and select Edit Process from the context menu.

In the "Edit Process" dialog box that opens, select a trigger in the following dropdown boxes:

When the user deletes, cancels, pauses, reassigns or opens a case, the selected trigger will be executed. The exact point when the trigger is executed depends on the action:

  • When deleting or canceling a case, the trigger is executed before the action takes place, so it is possible to stop the action by calling die() in the trigger. Note that deleting a case is only possible when in the the first task, meaning that the case is removed from the database. In subsequent tasks, it is only possible to cancel a case, meaning that the case remains in the database, but its status is changed to "CANCELLED" so it can no longer be opened and worked on.
  • When pausing or reassigning a case, the trigger is executed after the action takes place, so it is possible to stop the action by calling die(). However, it is possible to undo the reassignment of a case by calling Cases::reassignCases() in the trigger and reassigning it back to the original user. Likewise, it is possible to undo the pausing of a case by calling Cases::unpauseCase() in the trigger.
  • When opening a case, the trigger is executed after a new case is created or after an existing case is loaded, so it is possible to access the case's variables in the trigger. However, the trigger is executed before the case is displayed to the user, so it is possible to prevent the user from seeing the case by calling die() in the trigger. It is also possible to display a message above the case when it opens by calling G::SendMessageText().

When the trigger is executed, the @@APPLICATION system variable will be set to the unique ID of the case which is being deleted/canceled/paused/reassigned/opened, so it is possible to use executeQuery() to lookup information about that case in the database in tables such as APPLICATION, APP_CACHE_VIEW, APP_DELEGATION, APP_DOCUMENT, etc.

Example:
When a case is reassigned, the following trigger sends an email message to the user who is newly assigned to the case, plus a carbon copy to the user who was previously assigned to the case.

$caseId = @@APPLICATION;
//use order by descending to get the highest delegation index, which is the record for the reassigned case
$query = "select * from APP_CACHE_VIEW where APP_UID='$caseId' order by DEL_INDEX desc";
$aDels = executeQuery($query);
if (!is_array($aDels) or count($aDels) == 0) {
   G::SendMessageText("Error in query:\n$query", "WARNING");
}
else {
   //Case Info if needing to send it to template:
   $caseNo        = $aDels[1]['APP_NUMBER'];
   $newUserId     = $aDels[1]['USR_UID'];
   $prevUserId    = $aDels[1]['PREVIOUS_USR_UID'];
   $caseStartDate = $aDels[1]['APP_CREATE_DATE'];
   $aUser = PMFInformationUser($newUserId);
   $aPrevUser = PMFInformationUser($prevUserId);
   $to = "{$aUser['firstname']} {$aUser['lastname']} <{$aUser['mail']}>";
   $cc = "{$aPrevUser['firstname']} {$aPrevUser['lastname']} <{$aPrevUser['mail']}>";
 
   PMFSendMessage(@@APPLICATION, 'manager@example.com', $to, $cc, '',
       'Reassigned to case '.$caseNo, 'ReassignCaseTemplate.html');
}

Using PHP Functions

Any standard PHP function can be called inside triggers. Functions from PHP extensions can also be called if the extension is installed in the ProcessMaker server. In addition, it is also possible to define custom functions inside triggers. In the same trigger, where the function will be used, define the function: //define the custom function: function func() { //function code here } //call the function: func();

Example:
A custom function getAssignedCases() returns an array of the active cases assigned to a user. The function is called to get the number of cases assigned to the current logged-in user.

//Get the assigned cases for a specified user:
function getAssignedCases($userUid) {
   $userUid = mysql_real_escape_string($userUid);
   $query = "SELECT APP_UID, APP_NUMBER, DEL_INDEX, APP_TITLE FROM APP_CACHE_VIEW
     WHERE USR_UID='$userUid' AND DEL_THREAD_STATUS = "
OPEN" AND
     (APP_STATUS='TO_DO' OR APP_STATUS='DRAFT')"
;
   $aCases = executeQuery($query);

   if (!is_array($aCases)) {
      die("Error executing query:\n$query");
   }
   return $aCases;    
}    
       
@%numberOfCases = count(getAssignedCases(@@USER_LOGGED));

The getAssignedCases() function needs to be defined in the same trigger which calls it. If wishing to define it in a separate file, see Importing PHP files below.

Importing PHP files

PHP files can be imported into triggers by calling the require_once() function.

For example, to import the PHPExcel library, it can be downloaded and placed on the ProcessMaker server at the location:

/opt/phpexcel/PHPExcel.php
/opt/phpexcel/PHPExcel/Calculation.php
/opt/phpexcel/PHPExcel/Cell.php

Then, functions from this library can be used in a trigger by adding the following code at the beginning of the trigger:

require_once("/opt/phpexcel/PHPExcel.php");
//now functions from PHPExcel can be called in the trigger

Remember that require_once("/opt/phpexcel/PHPExcel.php"); should be added to every trigger which calls the PHPExcel functions, because there is no guarantee that the other triggers in the process have been executed.

It is also possible to reference libraries via their web URL. For example, if ProcessMaker is installed on a server at http://www.example.com and the PHPExcel files are placed at the location:

/opt/processmaker/workflow/public_html/phpexcel/PHPExcel.php
/opt/processmaker/workflow/public_html/phpexcel/PHPExcel/Calculation.php
/opt/processmaker/workflow/public_html/phpexcel/PHPExcel/Cell.php

Then, the following command can be used to import the PHPExcel library in a trigger:

require_once("http://www.example.com/phpexcel/PHPExcel.php");
//now functions from PHPExcel can be called in the trigger

Note: The problem with placing files in the processmaker/workflow/public_html directory is that they are publicly accessible, so it is not necessary to log into ProcessMaker to access them. Also, they will be overwritten every time ProcessMaker is upgraded to a new version, so it is recommended to place library files in a location outside the processmaker directory.

In order to import PHP files via a web URL, the allow_url_include setting needs to be enabled in the php.ini configuration file on the ProcessMaker server. Add the following line to the php.ini file: allow_url_include=1

Importing Public Files

If it is not possible to access the file system of the ProcessMaker server, the PHP code files can be uploaded as Public Files. Then, they can be imported into a trigger with the command: require_once("http://<IP-ADDRESS>/sys<WORKSPACE>/<LANG>/<SKIN>/<PROCESS-UID>/<FILENAME>"); //now functions from PHP file can be called in the trigger

PHP files uploaded as Public Files will not be overwritten when ProcessMaker is upgraded to a new version, but they are still publicly accessible, so do not use Public Files if the code in PHP files is confidential information.

For example, if the mycode.php file is uploaded in a process with the UID of 951278345568c24e3b3a2b4030387681 in the default "workflow" workspace on a server located at http://www.example.com, then it can be accessed with the following code in a trigger:

require_once("http://www.example.com/sysworkflow/en/neoclassic/951278345568c24e3b3a2b4030387681/mycode.php");
//now functions from mycode.php can be called in the trigger

To find the Process's unique ID, run a case with the Debugger turned on and check the value of the PROCESS variable.

The @@PROCESS system variable can be used to automatically insert the current process's unique ID in the URL, as long as the PHP file is uploaded in the Public Files of the same process:

require_once("http://www.example.com/sysworkflow/en/neoclassic/" . @@PROCESS . "/mycode.php");
//now functions from mycode.php can be called in the trigger

If the allow_url_include setting is not enabled (as explained above), then the public file can be imported directly from the server's file system. To import a public file which was uploaded to the same process which contains the trigger:

require_once(PATH_DATA_PUBLIC . @@PROCESS . PATH_SEP . 'mycode.php');
If accessing a public file which was uploaded to a different process, then specify the process' unique ID:
require_once(PATH_DATA_PUBLIC . '90867389156d629c03d98c1058762491' . PATH_SEP . 'mycode.php');
PATH_DATA_PUBLIC is a predefined constant containing the path to the public files, such as "/opt/processmaker/shared/sites/workflow/public/" on a Linux system. PATH_SEP is the directory separator, which is "/" on a Linux/UNIX system and "\" on a Windows system. Both of these constants are defined in workflow/public_html/sysGeneric.php.

Automatically importing code files

If not wishing to call require_once() in every trigger which references code in an external PHP file, then add the require_once() command to the processmaker/workflow/engine/classes/class.pmFunctions.php file, so the PHP file will automatically be imported in every trigger. However, the class.pmFunctions.php file will automatically be overwritten every time ProcessMaker is upgraded, so the require_once() command will need to be reinserted after every upgrade.

Timeout Triggers for Self Service Tasks

A timeout trigger can be set for Self Service and Self Service Value Based Assignment tasks, which will be executed if the task is not claimed by a user after a set amount of time. Timeout triggers are executed when the cron.php file on the ProcessMaker server is executed.

When a timeout trigger is executed, the system variables have not been updated for the current task, so the following system variables will not be accurate. They will be set to the values in the most recent task which was worked on in the case, which is generally the previous task before the self service task, but it could be a parallel task (if the process contains any parallel tasks):

  • @@TASK: Set to the ID of the task which was most recently worked on.
  • @%INDEX: Set to the delegation index of the task which was most recently worked on. To get the delegation index for the self service task, lookup it up in the APP_DELEGATION.DEL_INDEX field in the database. See the example below.
  • @@USER_LOGGED: Since the timeout trigger is executed by cron.php, there is no logged-in user, so @@USER_LOGGED is set to the ID of user who most recently worked on the case. If needing to get the ID of the user who worked on the task before the self service task, it is recommended to save the @@USER_LOGGED in a case variable in the that prior task. See the example below.
  • @@USR_USERNAME: Since the timeout trigger is executed by cron.php, there is no logged-in user, so @@USR_USERNAME is set to the ID of user who most recently worked on the case. If needing to get the ID of the user who worked on the task before the self service task, it is recommended to save the @@USR_USERNAME in a case variable in the that prior task. See the example below.

In a timeout trigger, the Cases::setCatchUser() method can be used to automatically assign a user to the task.

Example:

The following trigger saves the ID and username of the user who was assigned to the task prior to the self service task:

@@previousUserId = @@USER_LOGGED;
@@previousUsername = @@USR_USERNAME;

This trigger is set to fire before assignment in the task prior to the self service task.

Then, the following timeout trigger automatically assigns a Self Service task to the manager of the user who was assigned to the previous task in the process:

$taskId = '188586462577ed40a325599037697166'; //set to the ID of the self service task
$caseId = @@APPLICATION;
//lookup delegation index of self service task in database:
$sql = "SELECT DEL_INDEX FROM APP_DELEGATION WHERE APP_UID='$caseId' AND
   TAS_UID='$taskId' ORDER BY DEL_INDEX DESC"
;
$aDelegations = executeQuery($sql) or
   throw new Exception("Error in DB query: $sql");
if (count($aDelegations) == 0) {
   throw new Exception("Unable to find delegation index for self service task with query: $sql");
}
$index = $aDelegations[1]['DEL_INDEX'];
$aUser = userInfo(@@previousUserId); //user assigned to previous task
//if user does not have a Departmental manager, then assign the task to the admin:
if ($aUser['reportsto']) {
   $managerId = $aUser['reportsto'];
else
   $managerId = '00000000000000000000000000000001'; // ID of the admin

$c = new Cases();
$c->setCatchUser($caseId, $index, $managerId);

Example 2:

When assigning a user to a Self Service task, remember that the user must be in the assignment pool for that task. The following trigger randomly selects a user from the assignment pool for the Self Service task and assigns him/her to work on the task. Derivation::getAllUsersFromAnyTask() is used to get an array of the users assigned to the task and one is selected with the rand() function. In addition, the trigger uses PMFSendMessage() to send an email message to the assigned user to inform him/her of the assignment.

$taskId = '188586462577ed40a325599037697166'; //set to the ID of the self service task
$caseId = @@APPLICATION;
//lookup delegation index of self service task in database:
$sql = "SELECT DEL_INDEX FROM APP_DELEGATION WHERE APP_UID='$caseId' AND
   TAS_UID='$taskId' ORDER BY DEL_INDEX DESC"
;
$aDelegations = executeQuery($sql) or
   throw new Exception("Error in DB query: $sql");
if (count($aDelegations) == 0) {
   throw new Exception("Unable to find delegation index for self service task with query: $sql");
}
$index = $aDelegations[1]['DEL_INDEX'];
$d = new Derivation();
$aUsers = $d->getAllUsersFromAnyTask($taskId);
$cnt = count($aUsers);
if ($cnt > 0) {
   $userToAssign = $aUsers[rand(0, $cnt - 1)];
   $aUser = userInfo($userToAssign);
   $to = $aUser['firstname'].' '.$aUser[lastname].' <'.$aUser['mail'].'>';
   $c = new Cases();
   $c->setCatchUser(@@APPLICATION, $index, $userToAssign);
   PMFSendMessage(@@APPLICATION, 'admin@example.com', $to, '', '',
      "You're assigned to case #".@@APP_NUMBER, 'caseAssignment.html', array());
}

Any timeout triggers which are scheduled are executed when Unassigned cases is listed as DONE in the output of the cron.php file. For example, on a Linux server: # cd /opt/processmaker/workflow/engine/bin # php cron.php Processing workspace: workflow * Resending emails............................................[DONE] * Unpausing applications......................................[DONE] * Calculating Duration........................................[DONE] * Calculating Duration by Application.........................[DONE] * Executing events............................................[PROCESSING] * |- End Execution events.....................................[Processed 0] * Executing the scheduled starting cases......................[PROCESSING] [DONE] * Update case labels..........................................[DONE] * Unassigned case.............................................[DONE] * Executing cron files in bin/plugins directory in Workspace: workflow Finished 1 workspaces processed Done!

If an error occurs during the execution of a timeout trigger, an error message will replace the [DONE] in the output: * Unassigned case.............................................PHP Fatal error: Function name must be a string in /opt/processmaker/workflow/engine/classes/class.pmScript.php(210) : eval()'d code on line 9 Finished 1 workspaces processed Done!

When cron.php checks whether there are any timeouts scheduled, it adds the following line to the shared/log/cron.log file:

2016-12-02 22:36:02 |  | unassignedCase | action | Unassigned case

For every timeout trigger which is executed by cron.php, additional lines are added to the log file:

2016-12-02 22:36:02 |  | unassignedCase | action | Unassigned case
2016-12-02 22:36:03 |  | unassignedCase | action | OK Executed tigger to the case 62
2016-12-02 22:36:04 |  | unassignedCase | action | OK Executed tigger to the case 107
2016-12-02 22:36:05 |  | unassignedCase | action | OK Executed tigger to the case 123

Since timeout triggers are generally executed automatically by a cronjob in Linux or a Scheduled Task in Linux, it may be useful to catch exceptions and write their messages to a custom log file. For example, the above trigger could be written to print exceptions so the user can see them when cron.php executes and to write them to a custom log file named shared/log/timeoutExceptions.log:

try {
   $taskId = '188586462577ed40a325599037697166'; //set to the ID of the self service task
   $caseId = @@APPLICATION;
   //lookup delegation index of self service task in database:
   $sql = "SELECT DEL_INDEX FROM APP_DELEGATION WHERE APP_UID='$caseId' AND
      TAS_UID='$taskId' ORDER BY DEL_INDEX DESC"
;
   $aDelegations = executeQuery($sql) or
      throw new Exception("Error in DB query: $sql");
   if (count($aDelegations) == 0) {
      throw new Exception("Unable to find delegation index for self service task with query: $sql");
   }
   $index = $aDelegations[1]['DEL_INDEX'];
   $d = new Derivation();
   $aUsers = $d->getAllUsersFromAnyTask($taskId);
   $cnt = count($aUsers);
   if ($cnt == 0) {
      throw new Exception("No users assigned to self service task.");
   }
   $userToAssign = $aUsers[rand(0, $cnt - 1)];
   $aUser = userInfo($userToAssign);
   $to = $aUser['firstname'].' '.$aUser[lastname].' <'.$aUser['mail'].'>';
   $c = new Cases();
   $c->setCatchUser($caseId, $index, $userToAssign);
   PMFSendMessage($caseId, 'admin@example.com', $to, '', '',
      "You're assigned to case #".@@APP_NUMBER, 'caseAssignment.html', array());
}
catch (Exception $e) {
   $msg = "\nException in timeout trigger in case #".@@APP_NUMBER.' at '.date('Y-m-d_H:i:s').": \n".$e->getMessage();
   print $msg."\n"; //print in output of cron.php
   file_put_contents(PATH_DATA.'log'.PATH_SEP.'timeoutExceptions.php', $msg, FILE_APPEND);
}