CPMs: Triggering CPMs from Business Rules

UPDATE!

This functionality is now available in the May 2014 release! View the May 2014 release notes for details.

The Feature Gap

One nice feature that External Events used to have, which is currently unavailable for Custom Processes, was the ability to be triggered directly from Business Rules. I imagine whenever the rules system is next updated it will get a complete overhaul, and support for Custom Objects, Attributes, and Processes will all be added, but until then we can use this workaround.

The Solution

The key is to use a custom field as a trigger. I typically like to use a text or a menu field, so that simple data can be passed to and from the CPM. This works because the CPM is triggered regardless of how the record has been changed, which means that if you set the custom field in your business rules, the CPM will fire when the record is updated. You can also follow the opposite process to trigger and pass simple data to business rules from a CPM!

Halting Rules Until a CPM Has Processed

If you're savvy, using this procedure, you can 'halt' business rule processing on an object until the CPM logic has completed running. In your business rule, you set the trigger custom field, then transfer to a state and stop processing rules. The CPM gets triggered, and in your CPM logic you set another trigger (using a different value on the same field, or a new field altogether), and business rules get triggered again. It will take some tweaking to get things setup right, and I suppose there is the danger of running into loops, but with the right logic, you can essentially 'hook' into business rules to run Custom Processes.

Comments

I just received a question about triggering CPMs from a marketing campaign. A similar process can be used for this case, as well!

  1. In the Campaign editor, configure a 'Set Field' element to set a Contact custom field trigger (as described above)
  2. Configure your Contact CPM so that it conditionally runs logic, if the custom field trigger is set

Does this imply that you can have CPM only trigger on records which have a custom filed with a certainvalue? This would restrict the CPM running on all records except those with the specified custom field value?

That's not exactly what he's saying in this post, but yes you can. A CPM is simply custom PHP code triggered on certain object lifecycle events (Create, Update, Delete). Inside of the CPM you have access to the object that triggered the event (for the most part... there are a couple of bugs). You can use any standard conditional PHP structure to determine whether to execute custom logic or to simply 'return'.

ie
if($obj->CustomFields->c->my_cf !== "CX Developer")
{
return;
}

Not exactly. The CPM still fires depending on whether it is set for record update, create, or delete, but leveraging a custom field allows you to use conditional logic in your code depending on the state of the record.

For instance, what if you wanted to log to a CBO every time a contact was sent an email from a marketing campaign? In your campaign, if you set a custom field, say "campaign_triggered" to 'true', then you could do the following logic in your apply() method:

  1. If $contact->CustomFields->c->campaign_triggered is 'true'
    1. Log a new record to the CBO
    2. Set $contact->CustomFields->c->campaign_triggered to 'false' - IMPORTANT!
  2. Else don't do anything (the CPM still fires but your previous logic doesn't run)

Hi Ben,

I had a small question about custom processes. When using Business Rules you can always check whether a field was changed and what the previous value of that field was. I need to build a Custom process that generates a transaction in a custom object when certain fields are changed for a Contact. Do you know how I can check in CPMs if a field is changed and what the previous value was?

Cheers,
Bastiaan

ps. great articles on your site, nice to have some information outside the usual sources, keep up the good work!

Hi Bastiaan,

Thanks for joining! Very good question, so the quick answer is, yes, you can easily check what the previous value of a field is and if it has changed. Every Connect object (we'll call it $obj) that is passed to the CPM apply() method contains a 'prev' property, which is a copy of the previous state of the object. So you can compare the field values in the $obj with the values in $obj->prev to see what exactly was changed.

Now for a little more depth: An important detail to keep in mind with the update action, is that $obj only contains fields that have been changed. Any field that was not modified on save will not (usually) be populated. So, you could potentially simply look at the $obj values, and any that is populated was one that was modified. Here's the catch, if it was modified to a NULL or blank value, you can't tell the difference between a NULL value and an unpopulated field! So, you'll have to use the $obj->prev property regardless, for robustness. An additional complexity with this, is in your CPM logic, even if you do a fetch() on the object to attempt to load all fields, regardless of whether their values have been changed, because Connect for PHP has already cached the 'CPM' version of it, it won't even load the unchanged fields. What I've been forced to do in this situation, as horribly inefficient as it is, run an explicit ROQL query to retrieve the exact field I need to work with, which circumvents the cache and retrieves the true value in the system. I plan on writing a full-length article about this after I've inspected this behavior on various RightNow versions, and will update this thread with a link when complete.

We've also uncovered a product bug in that is related to this, and specific sub-objects in Connect for PHP when CPMs are in use, which is slated to be fixed: Updated: SP5 (in CX 13.2) Commit Bug w/ active CPM

I'm looking for a way of running CPM's (or anyway of getting to RNCPHP) on a scheduled basis, that doesn't require RN PS support to develop and manage via cron jobs etc.

I was thinking pattern A:
1. scheduled campaign->Scheduled Entry Point->Create Incident, setting a custom field called "Instruction".
2. CPM triggers on incident creation. Check for CF Instruction being non-null, switch/case/operate accordingly.

But I'm wary of this requiring a trigger on every INCIDENT creation - as we have thousands of incidents created, this seem like adding an extra CPM trigger overhead when 99.9% it wont be required.

Is there an alternative way ? Campaigns can't set CO's, only create Primary Objects and CF's within them.

plan B: using the more ugly custom scripts PHP within the header/footer of a scheduled REPORT ? But my quick tests seem to suggest that RNCPHP is not available from custom scripts in reports.

Any thoughts ?

iain.

Hi Iain,
Per A, there's really no way to conditionally trigger a CPM. They always fire on create, update, or delete for the object on which they are attached. But I have used them on some pretty high-volume sites, and the system seems to be handle the extra load as well as can be expected. But it all depends on how you code it. At the very top of apply(), you'll want to check for your conditional fields and return from the method if the code doesn't need to be run.

Per B, Connect for PHP is available in custom scripting! (I just tested on a May 2013 site.) Add this to your initialization function and you should be off and running:

require_once(get_cfg_var('doc_root') . '/include/ConnectPHP/Connect_init.phph');
initConnectAPI();

Another option C, for a more flexibility, would be to stand up a separate server, either on AWS or on your own infrastructure, which would run a Cron or Scheduled Task and use the SOAP API to connect to the RightNow instance. This would obviously take more effort to setup, though, and may need to pass your internal security review, etc.

Edit: It doesn't appear that custom scripts are run for scheduled reports, which is really too bad, as that would be a very powerful way to trigger events in the system. It looks like you'll have to go with option A or C. Both of these do have the added benefit of making the code more maintainable, since it's not hidden away in a report.

Hi Ben, and all avid readers.

Plan B is back on. Custom Scripts are proven to run OK within scheduled reports, and even better, when those reports have no recipients.

The tricks are twofold:
1. make sure you call putenv('TEMPORARY_RNW_CONF_DIRECTORY'); in the initialisation tab.
2. make sure you don't paste that line of code from the RightNow online help, as I did, and wasted two days beating my head off a brick wall and submitting defect incidents, inly to discover that the online help page somehow parsed quotes into angled single quotes, which are then silently rejected by the PHP parser.
putenv(‘TEMPORARY_RNW_CONF_DIRECTORY’);//is BAD
putenv('TEMPORARY_RNW_CONF_DIRECTORY');// is GOOD

I've then got my custom script calling RNCPHP and updating a row in a CO called Instructions (also setting last updated time into the bargain).

The plan is to then use RNCP triggers upon 'update' of "Instructions" to run the actual RNCPHP doing the donkey work.

So there we have it, a way of scheduling (to 15 minutes resolution) custom PHP.

Iain.

Any thoughts from others who've definitely got CPM's trigger on Update or Create of a CO ?

I have:
1. made sure I have defined the Package : Settings and Objects: Settings\Instructions - note this is the backslash notation as instructed on the online help, not the $ notation referred to in Ben's post on the '101' article.
2. I have stripped everything back to try to trigger on Create only, in case there was something funky about not all the object's fields being passed through (as referred to elsewhere).
3. I have stripped all back in case there was a runtime error elsewhere in the code that stopped an implicit commit.
4. tried making my activity to be the creation of a primary object (incident against a contact) rather than a modification of the CO, but no joy with that either.
5. All works fine in the TestHarness (all data is present in the $object).
6. Even commented out my exception handling and trace statement writes to phpoutlog() - as our appears to have blown up the RN console - Info Log button hangs RN console indefinitely. This was the case before my CPM foray. In case it appears I've created an infinite loop.
7. I make sure to check the cycle count and return if >0.
8. I've tried save() and save with suppressAll - no difference.

Any thoughts people ?!

Thanks in advance.


<?
/**
Declare the name of this Object Event Handler:
* CPMObjectEventHandler: instruction_trap_update_mini

Declare the package this Object Event Handler belongs to:
* Package: Settings

Declare the objects this Object Event Handler can handle:
* Objects: Settings\Instructions

Declare the actions this Object Event Handler can handle:
* Actions: Create

Declare the Connect Common Object Model version this
Object Event Handler is bound to:
* Version: 1.2
*/

/**
* @name instruction_trap_update_mini.php
* @author Iain McKay
* @since 25/02/2014
*
* @desc Traps update of the CO 'Instructions'
*/

// An alias to use for the version of the Custom Process Model
// this script is binding to:
use \RightNow\CPM\v1 as RNCPM;

// An alias use for the version of Connect for PHP that
// this script is binding to:
use \RightNow\Connect\v1_2 as RNCPHP;

/**
* This class contains the implementation of the Object Event Handler.
* It must be the same name as declared above in the
* CPMObjectEventHandler field in the header.
*/
class instruction_trap_update_mini implements RNCPM\ObjectEventHandler
{
/**
* The apply() method "applies" the effects of this handler
* for the given run_mode, action, object and cycle depth.
* Upon a successful return (no errors, uncaught exceptions, etc),
* a "commit" will be implicitly applied.
* @param[in] $run_mode may be one of:
* RNCPM\RunMode{Live,TestObject,TestHarness}
*
* @param[in] $action may be one of:
* RNCPM\Action{Create,Update,Destroy}
*
* @param[in][out] $object is the Connect for PHP object that was
* acted upon. If $action is Update or Destroy,
* then $object->prev *may* have the previous
* values/state of the object before $action was
* applied.
*
* @param[in] $n_cycles is the number of cycles encountered
* so far during this instance of $action upon
* $object.
*/
public static
function apply( $run_mode, $action, $object, $n_cycles )
{
//phpoutlog("IGM: instruction_trap_update::apply()\n");
if ($n_cycles !== 0) return;

//CREATE CASE
if (RNCPM\ActionCreate== $action)
{
/*if ($run_mode != RNCPM\RunModeLive)
{
echo( "--apply()-- \n" );
echo( "runmode:".$run_mode );
echo( "action:".$action );
echo( "this object:\n

".print_r($object)."

/n ");
}*/

try
{
// Do the hard work in here.
/*if ($run_mode != RNCPM\RunModeLive)
{
echo( "Instruction Custom Object passed in, ID:".$object->ID."\n" );
echo( "Instruction Custom Object passed in, Name:".$object->Name."\n" );
echo( "Instruction Custom Object passed in, Param1:".$object->Param1."\n" );
}*/

// hmm, assume that ID is the only known field, others may not be passed in at runtime (though they are in test mode)?
/*$strQuery=sprintf("SELECT Settings.Instructions FROM Settings.Instructions WHERE Settings.Instructions = ".$object->ID);
$arrDetails=array();
$arrRows = RNCPHP\ROQL::queryObject($strQuery)->next();
while($arrDetails = $arrRows->next()){
$arrDetails->Active = true;
$arrDetails->Request = true;
$arrDetails->Param3 = "trapped";
$arrDetails->save(RNCPHP\RNObject::SuppressAll);
}
*/

//if($object->ID != NULL)
//{
$object->Active = true;
$object->Request = true;
$object->Param3 = "trapped";
//$object->Param4 = "cycles:$n_cycles";
//$object->save(RNCPHP\RNObject::SuppressExternalEvents);
$object->save();
// }

/*
//create a new incident from empty by way of a means of proof that somethign happened !
$queryResult = RNCPHP\ROQL::queryObject("SELECT Contact FROM Contact WHERE Emails[0].Address = 'iain.mckay@thetrainline.com' AND Emails[0].AddressType.LookupName = 'Email - Primary'")->next();
$contact = $queryResult->next();
if(!$contact->ID && !$contact->ID > 0){
// Need to create a contact
$contact = new RNCPHP\Contact();
$contact->Emails = new RNCPHP\EmailArray();
$contact->Emails[0] = new RNCPHP\Email();
$contact->Emails[0]->AddressType=new RNCPHP\NamedIDOptList();
$contact->Emails[0]->AddressType->LookupName = "Email - Primary";
$contact->Emails[0]->Address = "iain.mckay@thetrainline.com";
$contact->Login = "iain.mckay@thetrainline.com";
$contact->Name = new RNCPHP\PersonName();
$contact->Name->First = "iain";
$contact->Name->Last = "mckay";
$contact->save(RNCPHP\RNObject::SuppressAll);
}
$c_id = (int)1616231;
$contact = RNCPHP\Contact::fetch($c_id);
$incident = new RNCPHP\Incident();
//now write an incident, first joining the incident to previously found/created contact.
$incident->PrimaryContact = $contact;
$incident->Subject = "trapped instruction";
$incident->StatusWithType = new RNCPHP\StatusWithType();
$incident->StatusWithType->Status = new RNCPHP\NamedIDOptList();
$incident->StatusWithType->Status->ID = 2;// for closed = solved

$incident->Threads = new RNCPHP\ThreadArray();
$incident->Threads[0] = new RNCPHP\Thread();
$incident->Threads[0]->EntryType = new RNCPHP\NamedIDOptList();
$incident->Threads[0]->EntryType->ID = 3; // Used the ID here. See the Thread object for definition
$incident->Threads[0]->Text = "trapped instruction";

$incident->CustomFields = new RNCPHP\IncidentCustomFields;
$incident->CustomFields->incident_type = new RNCPHP\NamedIDLabel();
$incident->CustomFields->incident_type->ID = 223;
$incident->CustomFields->ce_event_code = "help";
//$ret2 = $incident->save(RNCPHP\RNObject::SuppressAll);
$ret2 = $incident->save();
*/
}
catch(\Exception $err)
{
if ($run_mode == RNCPM\RunModeLive)
{
//phpoutlog("IGM: instruction_trap_update Exception: " . $err->getMessage() . "\n");
}
else
{
echo "IGM: instruction_trap_update Exception: " . $err->getMessage() . "\n";
}
RNCPHP\ConnectAPI::rollback();
return;
}
}

//no need for explicit commit? : RNCPHP\ConnectAPI::commit();
return;
} //end apply
}

/**
* This class contains the test harness for the Object Event Handler.
* It must be the same name as declared above but with "_TestHarness"
* added as a suffix on the name.
*/
class instruction_trap_update_mini_TestHarness implements RNCPM\ObjectEventHandler_TestHarness
{
/**
* setup() gives one a chance to do any kind of setup necessary
* for the test. The implementation can be empty, but it must exist.
*/
public static function setup()
{
echo( "--setup()-- \n" );
return;
} //end setup

/**
* fetchObject() is invoked by the test harness to get the set
* of objects to test with for the given action and object type.
* @param[in] $action may be one of:
* RNCPM\Action{Create,Update,Destroy}
*
* @param[in] $object_type is the PHP class name of the
* Connect object type being tested.
*
* \returns the object or an array of objects to test with.
*/
public static function fetchObject( $action, $object_type )
{
echo( "--fetchObject()--\n" );
echo( "Creating a test instruction:\n" );

//create an instruction test object
$instruction = new RNCPHP\Settings\Instructions();
$instruction->Name = "Test fetchObject";
$instruction->Active = true;
$instruction->Request = true;
$instruction->Param1 = "testing1";
$instruction->Param2 = "tetsing2";
$instruction->Param3 = "testing3";
$instruction->Param4 = "testing5";
$instruction->save(RNCPHP\RNObject::SuppressAll);

$obj = array($instruction);
return( $obj );
} //end fetchObject

/**
* validate() is invoked by the test harness to validate the
* expected effects of the given action upon the given object.
* Throw an exception or return false to indicate failure.
* @param[in] $action may be one of:
* RNCPM\Action{Create,Update,Destroy}
*
* @param[in] $object is the Connect for PHP object that was
* acted upon.
*
* \returns true if the test for $action, $object succeeded, or
* false otherwise.
* Throwing an exception is another way of communicating
* failure, and will display the exception text as a
* result of the test.
*/
public static function validate( $action, $object )
{
echo( "--validate()--\n" );

//The results need to be checked manually, so set pass to TRUE by default
$pass = TRUE;

//Print the email and login so they can be checked
echo( "Name: " . $object->Name . ", Param1: " . $object->Param1 . ", Last Updated: " . $object->UpdatedTime . "\n" );

//Destroy the instruction so we can create another for the next action
$object->destroy();

return( $pass );
} //end validate

/**
* cleanup() gives one a chance to do any kind of post-test clean up
* that may be necessary.
* The implementation can be empty, but it must exist.
* Note that the test harness is integrated with the Connect API such
* that operations performed thru the Connect API are not committed,
* so there's no need to clean up after the test even if it has created,
* modified or destroyed objects via the Connect API.
*/
public static function cleanup()
{
echo( "--cleanup()--\n" );
return;
} //end cleanup

}
?>

It's like it's not triggering on the CO in the first place, or some runtime error only affects the script in Live Mode not testing.

Update: Connect for PHP generated objects (standard and custom) trigger OK, but still not the SOAP API.

I can call Scheduled Report->CustomScript->RNCPHP->Save a Standard Object (Incident) with no suppression->triggers RNCPM OK. Here the incident.source = "Connect PHP"

I can use the same Scheduled Report->CustomScript->RNCPHP->Save a Custom Object with no suppression->triggers RNCPM OK. Here the incident.source = "Connect PHP"

I can call C#->SOAP API->Save a Standard Object (Incident) with no suppression (SuppressExternalEvents = false and SuppressRules = false )->does not trigger the same RNCPM as test 1 above. Here the incident.source = "Connect Web Services - SOAP"

Iain.

Hi Ben,

I believe that CPMs and business rules execute for all record changes with CPMs getting triggered after business rules finish processing. That means, after a business rule is called, CPM for that record is anyway going to get triggered without any update to that record within rules, right?

So, wouldn't your method just double up the triggering event and cause CPM to be called twice? Correct me if I'm wrong anywhere with my assumptions.

Thanks, Anuj

This functionality is now available in the May 2014 release! View the May 2014 release notes for details.

I've been attempting to have a CPM triggered from business rules (I've tried both create and update), but the CPM doesn't want to run. once I make an update to an incident via the console, however, the CPM does run.

Would there be any reasons as to why the CPM does not recognise and update or create through the business rules?

From business rule and add cpm event and do not trigger anything.

But since Trigger Update, if it works.

Should activate some thing?

Zircon - This is a contributing Drupal Theme
Design by WeebPal.