Free Events in Click & Pledge

Click & Pledge nicely supports online registration for both free and paid/ticketed events. However, one point of confusion for both users and staff is that a “$0.00” line item is displayed on the registration page even for free events, and for free events using the named event model, a “Payment Information” section is displayed on the second page of the registration UI.

Fortunately, C&P offers the ability to include custom CSS in the template designer. Some simple CSS suppresses these payment-related elements, granting a more streamlined free registration experience for both named and anonymous events. The effect isn’t perfect, but until Click & Pledge permits deeper modification of its registration component, I’m satisfied with the results. Enhancements are welcome!

Bridging Click & Pledge and Salesforce Campaigns with Process and Flow

This post has been extensively revised to broaden the applicability of the solution, covering anonymous and named Click & Pledge events.

Click & Pledge provides functionality to add contacts who register for an event to a Salesforce campaign. However, contacts are added with the first available status (often ‘Sent’), rather than a value showing them as registered or attended. Because campaigns provide a standard, deduplicated, reportable data architecture that’s independent of registration package, it’s valuable to propagate C&P status updates into the associated campaigns. Further, integrating registration information into campaigns increases the value of the Campaign History related list provided on every contact, offering an at-a-glance summary of recent invitations and responses thereto.

At the same time, we can add value to the Campaign Member object and support common reporting needs by marking who’s attending each event as a guest of whom, using a custom lookup field Registered by to the Contact on the Campaign Member object.

Because both of the registration objects used by Click and Pledge (C&P Event Registered Attendee and C&P Event Registrant) may be associated with C&P Temporary Contacts that can be linked to Contacts in any order, at different times, and via asynchronous process, we use an adaptable structure with two processes and two autolaunched flows. The processes and flows create and update Campaign Members incrementally as information becomes available, and re-run on each modification to the registration objects to propagate updates.

It’s also important to take into account anonymous events. Click & Pledge creates C&P Registered Attendee records for anonymous registrations, but they’re never linked to contacts — only the C&P Event Registrant is. Our flows take this variation into account.

Process 1: Registered Attendees

Triggered on the C&P Event Registered Attendee object upon creation or modification.

Because we cannot rely on a Contact ever being linked to the registered attendee (in the case of anonymous registrations), the process’s single action group is run without criteria. An update to a registered attendee without a contact attached may stem from a check-in event.

The process simply invokes Flow 1 with the Registered Attendee as its parameter. The flow is responsible for extracting the other required information from the object hierarchy.

Process 2: Registrants

Triggered on the C&P Event Registrant object upon creation or modification.

Even in the case of anonymous events, a Contact must ultimately be assigned to the registrant. This process therefore has a single action group criterion, [CnP_PaaS_EVT__Event_registrant_session__c].CnP_PaaS_EVT__ContactId__c is not null.

The only action is Run Flow, triggering Flow 2 with the Registrant ID as a parameter. This process is necessary not only to update the Registered by field, but also to allow our flows to source a Contact value from the registrant object for anonymous events.

Flow 1: Update Campaign Members

Called by Process 1 and Flow 2

This flow contains the meat of the functionality. It accepts one input variable, holding the ID of the C&P Event Registered Attendee. The flow has the following structure.

Update Campaign Members flow

Four Fast Lookup elements assign the C&P Event Registered Attendee, C&P Event, Campaign Member, and C&P Event Registrant objects to sObject variables. Note that the registered attendee is guaranteed not to be null by the calling process, but the other objects are nullable, and the registrant will often be null dependent upon the user’s processing of temporary contacts.

The flow uses five formulas to draw data from these four objects. Note that because the object hierarchy involves many nullable lookup fields and connections to contacts that may be populated in any order, we guard cross-object field references with null checks.

CampaignId

Evaluates to the Campaign assigned to the registration level, if any, or to that assigned to the event, if any, or null.

IF(!ISBLANK({!RegisteredAttendeeSobject.CnP_PaaS_EVT__Registration_level__c}) && !ISBLANK({!RegistrationLevelSobject.CnP_PaaS_EVT__Campaign__c}), {!RegistrationLevelSobject.CnP_PaaS_EVT__Campaign__c},
IF(!ISBLANK({!RegisteredAttendeeSobject.CnP_PaaS_EVT__EventId__c}) && !ISBLANK({!EventSobject.CnP_PaaS_EVT__Campaign__c}), {!EventSobject.CnP_PaaS_EVT__Campaign__c}, null))

ContactId

Evaluates to the Contact assigned to the registered attendee, unless the event is marked as anonymous, in which case it evaluates to the contact of the registrant. May be null if neither contact has been assigned.

IF(!ISBLANK({!RegisteredAttendeeSobject.CnP_PaaS_EVT__EventId__c}) && {!EventSobject.CnP_PaaS_EVT__Anonymous__c} && !ISBLANK({!RegisteredAttendeeSobject.CnP_PaaS_EVT__Registrant_session_Id__c}) && !ISBLANK({!Registrant.CnP_PaaS_EVT__ContactId__c}), {!Registrant.CnP_PaaS_EVT__ContactId__c}, {!RegisteredAttendeeSobject.CnP_PaaS_EVT__ContactId__c})

RegistrantId

Evaluates to the Contact assigned to the Registrant, unless it is the same as the Contact of the attendee. Used to populate the Registered by field.

IF(!ISBLANK({!RegisteredAttendeeSobject.CnP_PaaS_EVT__Registrant_session_Id__c}) && !ISBLANK({!Registrant.CnP_PaaS_EVT__ContactId__c}) && {!Registrant.CnP_PaaS_EVT__ContactId__c} != {!ContactId}, {!Registrant.CnP_PaaS_EVT__ContactId__c}, null)

CMStatusExisting

Evaluates to the campaign member status value to use if an existing Campaign Member is located. It will not overwrite an ‘Attended’ value; this copes with check-in events performed on the multiple registered attendees that may be linked to a single contact and registrant for anonymous events.

IF({!RegisteredAttendeeSobject.CnP_PaaS_EVT__CheckIn_Status__c} = 'Checked-In', 'Attended', IF(!ISPICKVAL({!CM.Status}, 'Attended'), 'Registered', TEXT({!CM.Status})))

CMStatusNew

Evaluates to the campaign member status value to use if a new Campaign Member is being created.

IF({!RegisteredAttendeeSobject.CnP_PaaS_EVT__CheckIn_Status__c} = 'Checked-In', 'Attended', 'Registered')

Structure

Because the calling processes cannot guarantee that appropriate information will be available in the object hierarchy in all circumstances, the flow uses a decision element to decide whether to proceed; it continues if both ContactId and CampaignId are non-null.

These conditions being met, if the Campaign Member is null, a new record is created and inserted, using CMStatusNew, ContactId, CampaignId, and RegistrantId. If not, the Campaign Member is updated, and CMStatusExisting is used.

If the registrant has not yet been populated (for a named event), the Registered by field will be populated when Process 2 and Flow 2 fire.

Flow 2: Update Registered Attendees

Called by Process 2, calls Flow 1

This flow is required for handling anonymous registrations (where a contact is assigned only to the registrant object) and for populating the “Registered by” field on the Campaign Member in the circumstance that the C&P Temporary Contact corresponding to the registrant is processed after one or more of those corresponding to the associated attendees. It accepts as input parameter the ID of the modified C&P Event Registrant object. The flow has the following structure.

Update Registered Attendees flow

A Fast Lookup assigns all C&P Registered Attendees linked to the registrant to an sObject collection variable. A loop iterates over the collection and invokes Flow 1 with the required parameter. No criteria are applied to the registered attendees; all evaluation is handled in Flow 1.

Summary

Processes and Flows offer an easy way to migrate Click and Pledge registration information to campaigns in real time. Unifying information about event registrations with campaigns enhances reportability, particularly in a context where multiple registration packages or types of event are in use (Click and Pledge, Google Forms, small events without online registrations), and allows staff to easily inspect event registrations on the Contact record, even without access to Click & Pledge.

Null Relationships and Short-Circuiting Behavior in Salesforce Formulas, Process Builder, Flow, and Apex

What happens when you refer to a field across a lookup relationship, and the lookup relationship is null? The answer turns out to vary across contexts in non-obvious ways. In the course of debugging some Process Builder logic, I came up with a summary. In all of the examples below, I’m using a custom object called Test Base Object with a nullable lookup relationship Account__c.

Object Setup

Formula Fields

When a cross-object reference is used in a formula field and the lookup is null, the value of the cross-object reference is also null. No exception is thrown.

Boolean logic treats the null value as false. Since no exception occurs, the ordering of Boolean clauses is irrelevant and no short-circuit evaluation is needed to obtain correct results.

Process Builder

Process Builder’s condition and formula-based triggers appear to operate like formulas, but actually handle null relationships very differently. Dereferencing a null field in a Process Builder condition or formula always results in an exception. With complex logic in conditions for running actions, this can be tricky to debug. The errors it produces for users are opaque and frustrating, often preventing any mutation of the involved object.

Process Builder Error

Fortunately, Boolean operators and functions (AND and OR, including the implicit logical operations used in condition-based triggers) in the Process Builder context perform short-circuit evaluation. In other words, references across the lookup relationship can be guarded by checks against null lookups earlier in the evaluation order such that evaluation will stop before reaching the cross-object relationship, avoiding an exception. The evaluation order is left-to-right for the AND() and OR() functions and the && and || operators, and top-to-bottom for condition lists.

Safe Process Builder Condition Pattern

Using conditions in Process Builder, always precede a cross-object field reference (assuming a nullable lookup relationship) with a null check. As in this example, protect the [Test_Base_Object__c].Account__c.Name reference with a preceding “Is null” condition on [Test_Base_Object__c].Account__c. Because the criteria are set to require “All of the conditions are met (AND)”, if Condition 1 evaluates to false (indicating a null value in the lookup field), evaluation of conditions will immediately stop, and no exception will be thrown.

Note that this won’t work the same way using an OR condition. OR short-circuits on true values, and short-circuiting because of a null lookup is often not the desired behavior. In many cases, it’s easier to handle possible null relationships by using customized logic and nesting an AND with the above null-check within the OR. Constructing a formula may be more straightforward.

Formulas in Process Builder short-circuit in the same way, whether using the AND() and OR() functions or the && and || operators. The following pattern is safe.

Safe Process Builder Formula Pattern

Flow

sObject variables in Flow present a challenge. While sObject variables can be null, and cross-object field references that traverse a null variable will result in an exception being thrown, one cannot directly test an sObject variable’s nullity within a Flow formula. ISBLANK(sObjectVariable) and ISNULL(sObjectVariable) aren’t legal and will prevent your Flow from being activated.

There are a couple of ways to work around this limitation.

One is to check the sObject variable’s nullity using a Decision element before using any formulas that references its fields. (See the release notes on cross-object references in Flow). Unfortunately, this may not be practicable in a flow where formulas make complex decisions or calculate across a number of different objects (see my discussion of bridging Click & Pledge with Salesforce Campaigns for an example).

Another option, if the variable is populated using a lookup element from a given Id value, is to check the nullity of the Id value field in the formula that performs the cross-object reference. Like in Process Builder, logical functions in Flow formulas use short-circuit evaluation. This allows you to effectively guard cross-object references against nulls in the circumstance that the potentially-null sObject variable is looked up from an Id field, rather than other criteria.

Finally, as discussed in the Summer ‘14 release notes, you can provide a fault path. While this offers less of an opportunity to handle decision-making or conditional data within a single formula, it permits clearly expressing error- and null-handling within the logic of the flow itself.

Apex

In most cases, Apex handles null relationships in the same way Process Builder formulas do. However, there’s one variant case: the code below does not crash.

Good Apex Code

Accessing the relationship path directly from the queried object simply results in a null; no exception is thrown.

However, this only works when you traverse the relationship via the queried sObject. If the intermediate object value (which is null) is assigned to another variable before dereferencing its field, you get a NullPointerException.

Like Process Builder formulas, Apex supports short-circuit evaluation. The code below outputs null, null, and false before finally throwing an exception at line 10.

Bad Apex Code

Dreamforce

Dreamforce this year was a fantastic experience.

  • I came home with two certifications, Platform Developer I and Administrator.
  • My talk with Adam Kramer, Staying on Top of Salesforce & NPSP Releases, had a nice reception, and I got to chat with some excellent nonprofit Salesforce users.
  • I learned a ton of new techniques for writing generic and dynamic Apex code, using mocks and stubs to build better unit tests, implementing continuous integration, and practicing modern development on the Salesforce platform.

I can’t wait to go back.

A year of officiating

I’m very happy to have been elected to another year as Head of Officials for my roller derby league, Duke City Roller Derby. I think we’ve made a lot of progress in 2016 and I’m grateful that DCRD took a chance on me, then a very new NSO, last year.

It’s been a busy and exhilarating year. I’ve officiated 51 games in six states, participated in 3 tournaments, traveled (by a back-of-the-envelope reckoning) something on the order of 10,000 miles, met tons of fantastic people, and had a great time. My personal goal for 2017 is to join at least two higher-level tournaments and aim for WFTDA certification.