+ Start a Discussion
e r i c.ax249e r i c.ax249 

Salesforce for Outlook: WhoId and WhatId NULL in Before and After Task Triggers

Hello all.  We have a trigger on the Task object that looks at the WhatId and/or WhoId to update certain fields on the related Account.  A user reported an issue to us when adding emails to Account and Contact records.  Upon taking a look at the debug logs, the WhatId and WhoId are both NULL in Before and After triggers.  However, after the Task record is saved, the WhatId and WhoId are properly set.  The logic of the trigger does not execute properly since the values are NULL when the trigger is executing.  Even if I perform a query in the After trigger for the WhatId and WhoId, they are still NULL.


How does Salesforce for Outlook work regarding the WhatId and WhoId?


System.debug commands:

	for (Task record : Trigger.new) {
		System.debug([SELECT Id, WhoId FROM Task WHERE Id = :record.Id].WhoId);

 Result in debug log:

15:26:09.682 (682782000)|USER_DEBUG|[13]|DEBUG|null
15:26:09.682 (682788000)|SYSTEM_METHOD_EXIT|[13]|System.debug(ANY)
15:26:09.682 (682839000)|SYSTEM_METHOD_ENTRY|[14]|System.debug(ANY)
15:26:09.682 (682850000)|USER_DEBUG|[14]|DEBUG|null
15:26:09.682 (682856000)|SYSTEM_METHOD_EXIT|[14]|System.debug(ANY)
15:26:09.682 (682940000)|SYSTEM_METHOD_ENTRY|[15]|System.debug(ANY)
15:26:09.682 (682953000)|USER_DEBUG|[15]|DEBUG|Completed
15:26:09.682 (682959000)|SYSTEM_METHOD_EXIT|[15]|System.debug(ANY)
15:26:09.683 (683169000)|SOQL_EXECUTE_BEGIN|[16]|Aggregations:0|select Id, WhoId from Task where Id = :tmpVar1
15:26:09.700 (700279000)|SOQL_EXECUTE_END|[16]|Rows:1
15:26:09.700 (700390000)|SYSTEM_METHOD_ENTRY|[16]|System.debug(ANY)
15:26:09.700 (700398000)|USER_DEBUG|[16]|DEBUG|null




Hi Eric,


Salesforce for Outlook currently doesn't populate the Related to:(whoID and WhatID) for Tasks and events when syncing from Outlook to Salesforce.

Which is the reason we have the unresolved items where the salesforce user has to manually associate the Task/Event with a particular Account or Contact.


There are some enhancements coming in specific to Event Association in Salesforce for Outlook which you can check in the release notes: page 86:


e r i c.ax249e r i c.ax249

Thanks for the reply Sonam. The part I want to understand is why the tasks do not end up in the Unresolved Items. The tasks are created properly linked with the WhatId and WhoId automatically associated with it. At what point does the WhatId and WhoId get assigned? The CreatedDate and LastModifiedDate fields are identical, so I would expect that an After trigger would be able to select the WhatId and WhoId.

B E N S.ax1881B E N S.ax1881

Hi Eric,


We're having the same issue. We have a trigger which checks if the WhoId is populated however the field is always NULL when synced/created from Outlook, even after update.


Did you manage to find a solution?





e r i c.ax249e r i c.ax249

Hello Ben,


The workaround I implemented was to have the trigger check to see if WhoId and WhatId were both NULL.  If so, the trigger would pass the task Ids to an @future method that would then check to see if WhoId or WhatId was populated.


Let me know if I was not clear in the explanation.




B E N S.ax1881B E N S.ax1881

It works perfectly - thanks a lot Eric!!


Dhananjay Patil 12Dhananjay Patil 12
Can u provide the  code? 
Kelly KKelly K

I completely forgot I was following this thread, but I saw Dhananjay's question and felt it's a good time to weigh in considering the experience I recently went through.

Our use case was that we wanted to do additional updates whenever a user logs a task, either via Salesforce for Outlook or manually. If the task is logged manually, the WhoId and WhatId are immediately available. If the task is logged via Salesforce for Outlook, this is the tricky part. At one point, yes, I did try Ben's suggestion where you write the trigger to look for tasks coming in where both the WhoId and WhatId were blank and queue this up for an @future method for later processing. It worked for a time and then it stopped.

After a very lengthy discussion and case that was opened with Salesforce support, this solution is not viable. Why? The @future method is asynchronous.

When SFO creates a task, it does it in several stages. It creates the base of the task and then later comes in with another DML to update the task with the Who and WhatIds. The problem with the @future method, is often it can start before the the 2nd DML comes in from the first process and updates the task with the Who and WhatId. You're left with zero results and a broken process and a pretty frustrated developer (hi!).

The best solution we were able to come up with, is to set up a batch process that runs every 5 minutes on all new tasks that have come in (with a 1 minute offset so the ones that came in right before the task kicked off have sufficient time to populate the WhoId and WhatId). In my case, I set up some workflow rules on the task to mark them with a date/time stamp of when the task was officially closed so I had something that was more reliable than the last modified date and a checkbox to queue an item up. Yes, I had to log a case with SF support to convert the checkbox into an indexed field so that my query in the batch class was selective. Another caveat to this - workflow rules do not run on tasks that are associated with emails (such as using 'send an email' on the case). You'll need to write apex code to address those and mimic the behavior of a workflow rule if it's important to you.

Here are my code samples for those who find themselves in a similar boat. Hope this helps,


global class TaskUpdateActivityInfoBatch implements Database.Batchable<sObject>, Database.Stateful, Schedulable {

	Integer failedUpdates {get; set;}

	global TaskUpdateActivityInfoBatch() {
		failedUpdates = 0;

	//Allows the class to be scheduled
	global void execute(SchedulableContext SC) {
		Database.executeBatch(new TaskUpdateActivityInfoBatch());

	//Method to schedule through apex/developer console
	/* Request is every 5 minutes, so need to set up several different jobs through execute anonymous. Here's the block that can be used to set them:
	TaskUpdateActivityInfoBatch.scheduleMe('0 0 * ? * *', 0);
	TaskUpdateActivityInfoBatch.scheduleMe('0 5 * ? * *', 5);
	TaskUpdateActivityInfoBatch.scheduleMe('0 10 * ? * *', 10);
	TaskUpdateActivityInfoBatch.scheduleMe('0 15 * ? * *', 15);
	TaskUpdateActivityInfoBatch.scheduleMe('0 20 * ? * *', 20);
	TaskUpdateActivityInfoBatch.scheduleMe('0 25 * ? * *', 25);
	TaskUpdateActivityInfoBatch.scheduleMe('0 30 * ? * *', 30);
	TaskUpdateActivityInfoBatch.scheduleMe('0 35 * ? * *', 35);
	TaskUpdateActivityInfoBatch.scheduleMe('0 40 * ? * *', 40);
	TaskUpdateActivityInfoBatch.scheduleMe('0 45 * ? * *', 45);
	TaskUpdateActivityInfoBatch.scheduleMe('0 50 * ? * *', 50);
	TaskUpdateActivityInfoBatch.scheduleMe('0 55 * ? * *', 55);
	global static String scheduleMe(String schedule, Integer i) {
		TaskUpdateActivityInfoBatch batchJob = new TaskUpdateActivityInfoBatch();
		String batchName = Test.isRunningTest() ? 'TaskUpdateActivityInfoBatch-RunningTest-' + i : 'TaskUpdateActivityInfoBatch-' + i;
		return system.schedule(batchName, schedule, batchJob);

	global Database.QueryLocator start(Database.BatchableContext BC) {
		DateTime currentTimeOffSet = system.now().addMinutes(-1);
		//Using custom field Owner_Role_Id__c instead of Owner.UserRoleId in query because test classes seem to be unable to pull that info (returns null every time).
		return Database.getQueryLocator([SELECT Id, Owner_Role_Id__c, WhoId, WhatId, AccountId, Date_Time_Processed__c, In_Queue_for_Processing__c, Prospecting_call_connected__c, Prospecting_call_affirmative__c, Email_Connection__c, qbdialer__ImpressionId__c FROM Task WHERE In_Queue_for_Processing__c = TRUE AND Date_Time_Processed__c <= :currentTimeOffSet ORDER BY Date_Time_Processed__c ASC]);

   	global void execute(Database.BatchableContext BC, List<sObject> scope) {
		List<Apex_Error__c> apexErrors = new List<Apex_Error__c>();

		try {
		catch(Exception e) {
			for(Task task : (List<Task>)scope) {
				apexErrors.add(new Apex_Error__c(Apex_Class__c = 'TaskUpdateActivityInfoBatch', Error_Record__c = task.Id, Error_Details__c = e.getMessage()));

		if(apexErrors.size() > 0)
        	insert apexErrors;

	global void finish(Database.BatchableContext BC) {
		AsyncApexJob job = [SELECT Id, ApexClassId, JobItemsProcessed, TotalJobItems, NumberOfErrors, CreatedBy.Email
							FROM AsyncApexJob WHERE Id = :BC.getJobId()];

		if(failedUpdates > 0 || job.NumberOfErrors > 0) {
			String emailMessage = 'Batch Job TaskUpdateActivityInfo has errors. <br/><br/>'
								 +'Number of batches: ' + job.TotalJobItems + '<br/>'
								 +'Number of successful batches: ' + job.JobItemsProcessed + '<br/>'
								 +'Number of unsuccessful batches: ' + job.NumberOfErrors + '<br/>'
								 +'Number of unsucessful record updates: ' + failedUpdates + '<br/><br/>'
								 +'For more details, please review the Apex Errors tab.';

			Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();

			//Uses custom setting to add other email addresses
			String[] toAddresses = new String[] {job.CreatedBy.email};

			Map<String, Apex_Distribution_List__c> distList = Apex_Distribution_List__c.getAll();
	        String serverhost = URL.getSalesforceBaseUrl().getHost().left(2);

	        for(Apex_Distribution_List__c record : distList.values()) {
	            //If it's a produciton environment, add only system admin emails
	            if(serverHost == 'na' || Test.isRunningTest())
	            //Otherwise, only add those that are enabled for sandbox alerts
	            if(serverHost != 'na' || Test.isRunningTest())

			mail.setSenderDisplayName('Salesforce Apex Batch Job Summary');
			mail.setSubject('Salesforce Batch Job Summary: Task Update Activity Info');
			Messaging.sendEmail(new Messaging.SingleEmailMessage[] {mail});

Apex Class:

public with sharing class TaskUpdateActivityInfo {

	/*Other major components to this class:
	 * Workflow rules on contact & lead: Triggering action is "Have a Sales Rep Contact Me" set to Yes
	 * -- Workflow rule will clear out the value in First Connection Date/Time
	 * -- Workflow rule will clear out the value in First Activity Date/Time
	 * -- Workflow rule will clear out the value in First Activity Id
	 * -- Workflow rule will update the value in Date Requested Sales Follow Up to NOW()
	 * Class method on task (before trigger): Triggering action is created with a subject of "Email:Re:"
	 * -- will update "Email Connection" to true
	 * Please note that this class should not be tied to a trigger due to the asyncronous nature of tasks created by Salesforce for Outlook.*/

	//This method determines whether the updates should apply to a contact/account, lead, or neither
	public static void determineObjType(List<Task> tasksForProcessing) {
		//Map is by sObject record Id, then with the lists of tasks for that sObject record
		Map<Id, List<Task>> tasksOnLeads = new Map<Id, List<Task>>();
		Map<Id, List<Task>> tasksOnContacts = new Map<Id, List<Task>>();
		Map<Account, List<Task>> tasksOnAccounts = new Map<Account, List<Task>>();
		List<Task> tasksForUpdate = new List<Task>();

		//need to determine if the task is on a contact or a lead and place them into a map
		for(Task task : tasksForProcessing) {
			//Update the task to indicate it has been processed
			task.In_Queue_for_Processing__c = false;

			//If the task is not related to anything, continue
			if(task.WhoId != null || task.WhatId != null) {
				//If has Account: Tasks where only WhoId is provided have an AccountId, so this covers cases where either an account WhatId is provided or a contact WhoId.
				if(task.AccountId != null) {
					List<Task> accountTaskList = new List<Task>();
					Account account = new Account(Id = task.AccountId);

						accountTaskList = tasksOnAccounts.get(account);

					tasksOnAccounts.put(account, accountTaskList);

				//If Contact
				if(task.WhoId != null) {
					if(String.valueOf(task.WhoId).startsWith('003')) {
						List<Task> contactTaskList = new List<Task>();

							contactTaskList = tasksOnContacts.get(task.WhoId);

						tasksOnContacts.put(task.WhoId, contactTaskList);			}

					//If Lead
					else if(String.valueOf(task.WhoId).startsWith('00Q')) {
						List<Task> leadTaskList = new List<Task>();

							leadTaskList = tasksOnLeads.get(task.WhoId);

						tasksOnLeads.put(task.WhoId, leadTaskList);




			update tasksForUpdate;

	public static void updateContact(Map<Id, List<Task>> tasksOnContacts) {

	public static void updateLead(Map<Id, List<Task>> tasksOnLeads) {

	public static void updateAccount(Map<Account, List<Task>> tasksOnAccounts) {


Dhananjay Patil 12Dhananjay Patil 12
@Kelly K,Thanks for the update.I have posted teh simpilar requirement before:


In my requirement I have created a WF to update the field on the task. whenever the task is created.In my case when I add an email from outlook to SFDC,it creates a task on contact under Activity History but when i drilldown on it the Field value that i want to populate is display as blank because WF rule is not triggering in case of outlook..
u can check my question which is kinda similar to ur requirement.
and thanks for providing the coding sample...