Custom Workflow Activity for matching email addresses to customers


In an earlier blog (E-mail to Case/Lead Using CRM 4 Workflow ), I described a sample workflow that could be used to automatically create cases based on emails sent to a queue.

One of the questions that was frequently asked for the above blog was that the email activity’s regarding field set to blank. In this blog, I’ll show how this can be done using a custom CRM workflow activity.

Let’s summarize the problem we are trying to solve. Joe (joe@mycustomer.com) is a customer in your system and say you have an account that is capturing this. Joe sends an email to your support alias to report an issue. We would like the email activity that is created to be regarding Joe’s account.

For those of you new to writing custom workflow activities, I highly recommend that you take a look at the SDK documentation at: http://msdn.microsoft.com/en-us/library/cc151142.aspx –It contains all the information needed for you to start using custom workflow activities.

I’m going to split my solution into 2 parts:

1) A custom workflow activity that takes an email address as input and returns a matching account as output

2) A workflow rule that uses the above custom activity. This workflow rule is triggered when an email is created and updates the email activity to have the regarding field set to the output returned by the custom workflow activity.

Custom Workflow Activity

Let’s walk thru’ the code for the custom workflow activity.

1) Define the class skeleton and the using statements for the various namespaces that we will need. Please ensure you have read and understood the SDK documentation at: http://msdn.microsoft.com/en-us/library/cc151142.aspx to get an overview of how to develop CRM custom workflow activities.

using System.Workflow.Activities;

using System.Workflow.ComponentModel;

using Microsoft.Crm.Sdk;

using Microsoft.Crm.SdkTypeProxy;

using Microsoft.Crm.Workflow;

using Microsoft.Crm.Sdk.Query;

namespace CrmCustomWFActivity

{

   [CrmWorkflowActivity("Find Customer with specified email address")]

   public class MatchSenderWithExistingCustomerActivity:  

   System.Workflow.Activities.SequenceActivity

   {

       // Activity code goes here.

   }

}

2) Now, add properties that represent the inputs and outputs. As discussed earlier, the input is an email address (of type string) and the output will be the matching account (of type Lookup).

DependencyProperty is a Windows Workflow concept and you can read more about it here: http://msdn.microsoft.com/en-us/library/system.workflow.componentmodel.dependencyproperty.aspx

   1: // Input property
   2: public static DependencyProperty senderProperty = DependencyProperty.Register("sender", typeof(string), typeof(MatchSenderWithExistingCustomerActivity));
   3:  
   4:         [CrmInput("Sender")]
   5:         public string sender
   6:         {
   7:             get
   8:             {
   9:                 return (string)base.GetValue(senderProperty);
  10:             }
  11:             set
  12:             {
  13:                 base.SetValue(senderProperty, value);
  14:             }
  15:  
  16:         }
  17:  
  18: // Output property
  19: public static DependencyProperty accountIdProperty = DependencyProperty.Register("accountId", typeof(Lookup), typeof(MatchSenderWithExistingCustomerActivity));
  20:  
  21:         [CrmOutput("AccountId")]
  22:         [CrmReferenceTarget("account")]
  23:         public Lookup accountId
  24:         {
  25:             get
  26:             {
  27:                 return (Lookup)base.GetValue(accountIdProperty);
  28:             }
  29:             set
  30:             {
  31:                 base.SetValue(accountIdProperty, value);
  32:             }
  33:  
  34:         }

3) This custom workflow activity needs to be able to retrieve accounts whose email address matches the sender property value. To do this, it needs to be able to call into CRM SDK methods and can do this thru’ IcrmService interface that is provided to all crm custom workflow activities. The following is code for a helper method that uses CRM query functionality to return account that matches the email address.

   1: private Guid MatchSenderWithExistingAccount(ICrmService crmService, string fromAddress)
   2: {
   3:     // Retrieve accounts with this email address.
   4:     QueryByAttribute query = new QueryByAttribute();
   5:     query.EntityName = EntityName.account.ToString();
   6:     query.Attributes = new string[] { "emailaddress1" };
   7:     query.Values = new string[] { fromAddress };
   8:  
   9:     RetrieveMultipleRequest retrieveMultipleRequest = new RetrieveMultipleRequest();
  10:     retrieveMultipleRequest.Query = query;
  11:     retrieveMultipleRequest.ReturnDynamicEntities = true;
  12:  
  13:     RetrieveMultipleResponse retrieveMultipleResponse = (RetrieveMultipleResponse)crmService.Execute(retrieveMultipleRequest);
  14:  
  15:  
  16:  
  17:     Guid accountId = Guid.Empty;
  18:  
  19:     foreach (BusinessEntity busEntity in retrieveMultipleResponse.BusinessEntityCollection.BusinessEntities)
  20:     {
  21:         // Pick the first accountid.
  22:         accountId = ((Key)(((DynamicEntity)busEntity)["accountid"])).Value;
  23:         break;
  24:     }
  25:  
  26:     return accountId;
  27: }

4) Now, we can implement the Execute method of the custom workflow activity.

   1: protected override System.Workflow.ComponentModel.ActivityExecutionStatus Execute(System.Workflow.ComponentModel.ActivityExecutionContext executionContext)
   2:         {
   3:             IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
   4:             IWorkflowContext context = contextService.Context;
   5:  
   6:         // Obtain IcrmService so we can call into CRM SDK to retrieve
   7:         // accounts
   8:             ICrmService crmService = context.CreateCrmService(); 
   9:  
  10:             // this.sender property will have the email address that needs to be matched.
  11:             Guid accountId = MatchSenderWithExistingAccount(crmService, this.sender);
  12:  
  13:             // Set the accountId output property to return this data
  14:         // back to the calling workflow
  15:             this.accountId = new Lookup("account", accountId);
  16:  
  17:             return ActivityExecutionStatus.Closed;
  18:         }
  19:       
  20:     return accountId;

Using The Custom Workflow Activity

In order to use MatchSenderWithExistingCustomerActivity in workflow rules, you need to register the activity. Follow the steps at: http://msdn.microsoft.com/en-us/library/cc151144.aspx to accomplish this.

We will create a workflow rule that does the following:

1) When an email is created, it checks if the email’s regarding field is set.

2) If not set, it invokes MatchSenderWithExistingCustomerActivity

3) It updates email with the accountid returned by MatchSenderWithExistingCustomerActivity.

Let’s walk through these in more detail.

1) Create the workflow rule, call it SetEmailRegarding and set it to trigger on email create

clip_image002

2) Insert a check condition step and configure it to check if Email:regarding field does not contain data

clip_image004

3) If Email:regarding is not set, we want to invoke the MatchSenderWithExistingCustomerActivity. Under the add step drop-down, you should find the custom workflow activity as shown below:

clip_image006

4) After you select MatchSenderWithExistingCustomerActivity, you need to ensure that it is being invoked with the correct input. Click ‘Set Properties’ and set the value of the input parameter Sender to that of Email:Sender

clip_image008

5) The next step would be to update the email’s regarding to the value returned by the custom workflow activity in the previous step. To do this, add a Update Email step, select ‘Set Properties’ and tab to the ‘Regarding’ field. In the dynamic values form assistant, you will now find that it shows ‘Match email address:accountid’ to indicate the return value from the previous step. Select this to be the value for regarding field as shown below.

clip_image010

6) Publish the workflow and verify that it is working.

Since the functionality to match email address to an account has been captured as a custom workflow activity, you will be able to use that activity in all your workflow rules. You can also enhance the custom workflow activity by making it return either a matching account or contact if needed. If you need to create an account/contact if a match wasn’t found, you can do this by adding a Create step to the workflow rule.

Jagan Peri

Comments (24)

  1. donnydanyluk says:

    I know that I am asking in the wrong spot but I don’t know where else to look. I have CRM 4.0 and I have an unassigned que that my cases go into. I want to have the case auto-assign to a specific user that opens it. So if I am user Bob and I open a case in the unassigned que it will automatically assign the case to me. Is this possible?

  2. Gabriel says:

    Hi,

    It’s great, i was trying to do the same.

    But I still has an error when trying to register the workflow with The Plugin Registration Tools.

    <description>The type Lookup of the property contactId is not supported. </description>

    It work find with a string but not a Lookup.

    Any Idea ?

  3. jagan_peri says:

    Hi Gabriel,

      How are you declaring this lookup? Can you include your definition?

    Lookups should work if you follow my code sample posted above…

    thanks

    Jagan Peri

  4. Ant Holloway says:

    Hi,

    First off, thanks for writing this post as it provides some much-requested functionality and another sample to work from.

    However, I cannot seem to get it to work.  The sample code above seems to have a return statement in the wrong place:

    3rd box, line 20:

    return accountId;

    With that line removed the code compiles and registers fine, though it stalls any workflows it is part of.  With that line in, it refuses to compile.  Any ideas?

  5. MindenMan says:

    Hi;

    Do you have your code example?

    I can’t get the project to compile, it keeps asking for a reference for Guid.  I have added only the references you have specified above apart from Microsoft.Crm.Workflow as I can’t seem to find that so I assume it is included somewhere else?

    Thanks

    MM

  6. MindenMan says:

    Can you please check your code?

    I only got this to build by adding "using System;" for reference and also deleting line number 20 on your last snippet "return accountId;"

    If I am correct then you at least owe it to others to correct your mistake(s).

    MM

  7. MindenMan says:

    Hi all;

    As previously mentioned the initial code kindly posted by Jagan requires two changes to work.

    Also, a lot of CRM customers want to associate emails with contacts rather than just accounts as there are usually lots of contacts per account.  And the above code will only work if the the value in the "from" field matches the email address on the account, this is not ideal in the real world as one can get emails from multiple people from within a company (account). The code below will  enable you to create a plug-in that will set the regarding field in an email to that of the contact who sent it – assuming it exists in the first place

    As with everything like this, usual caveats apply, not supported, back up first, test in development first etc etc etc:

    using System;

    using System.Workflow.Activities;

    using System.Workflow.ComponentModel;

    using Microsoft.Crm.Sdk;

    using Microsoft.Crm.SdkTypeProxy;

    using Microsoft.Crm.Workflow;

    using Microsoft.Crm.Sdk.Query;

    using System.Workflow.Runtime;

    using System.Data;

    using System.Xml;

    using System.Xml.Linq;

    namespace MatchContactSenderEmail

    {

       [CrmWorkflowActivity("Find Contact with specified email address")]

       public class MatchSenderWithExistingContactActivity :

       System.Workflow.Activities.SequenceActivity

       {

           // Input property

           public static DependencyProperty senderProperty = DependencyProperty.Register("sender", typeof(string), typeof(MatchSenderWithExistingContactActivity));

           [CrmInput("Sender")]

           public string sender

           {

               get

               {

                   return (string)base.GetValue(senderProperty);

               }

               set

               {

                   base.SetValue(senderProperty, value);

               }

           }

           // Output property

           public static DependencyProperty contactIdProperty = DependencyProperty.Register("contactId", typeof(Lookup), typeof(MatchSenderWithExistingContactActivity));

           [CrmOutput("ContactId")]

           [CrmReferenceTarget("contact")]

           public Lookup contactId

           {

               get

               {

                   return (Lookup)base.GetValue(contactIdProperty);

               }

               set

               {

                   base.SetValue(contactIdProperty, value);

               }

           }

           private Guid MatchSenderWithExistingContact(ICrmService crmService, string fromAddress)

           {

               // Retrieve accounts with this email address.

               QueryByAttribute query = new QueryByAttribute();

               query.EntityName = EntityName.contact.ToString();

               query.Attributes = new string[] { "emailaddress1" };

               query.Values = new string[] { fromAddress };

               RetrieveMultipleRequest retrieveMultipleRequest = new RetrieveMultipleRequest();

               retrieveMultipleRequest.Query = query;

               retrieveMultipleRequest.ReturnDynamicEntities = true;

               RetrieveMultipleResponse retrieveMultipleResponse = (RetrieveMultipleResponse)crmService.Execute(retrieveMultipleRequest);

               Guid contactId = Guid.Empty;

               foreach (BusinessEntity busEntity in retrieveMultipleResponse.BusinessEntityCollection.BusinessEntities)

               {

                   // Pick the first contactid.

                   contactId = ((Key)(((DynamicEntity)busEntity)["contactid"])).Value;

                   break;

               }

               return contactId;

           }

           protected override System.Workflow.ComponentModel.ActivityExecutionStatus Execute(System.Workflow.ComponentModel.ActivityExecutionContext executionContext)

           {

               IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));

               IWorkflowContext context = contextService.Context;

               // Obtain IcrmService so we can call into CRM SDK to retrieve

               // accounts

               ICrmService crmService = context.CreateCrmService();

               // this.sender property will have the email address that needs to be matched.

               Guid contactId = MatchSenderWithExistingContact(crmService, this.sender);

               // Set the accountId output property to return this data

               // back to the calling workflow

               this.contactId = new Lookup("contact", contactId);

               return ActivityExecutionStatus.Closed;

           }

       }

    }

  8. Hello,

    Thanks to MindenMan & Jagan Peri for the code. I use the code post by MindenMan and it works. Curiously the dynamic value set for the sender field appear twice after being save and reopen. I’m I the only one getting this behaviour in the Workflow sender field?

  9. James says:

    Hello.

    I use the workflow earlier described to make a case from email activity and get the same problem: the cases are not connected to a customer.

    If I use the manual command: "convert email to case" directly on the email-activity, the case then get the correct customer.

    Isn’t there a way to to this without creating theese custom workflows?

    James

  10. I have a question. I have created customized workflow using visual studio and have attached it to the SharePoint list. The problem I am getting is when I modify my workflow code and reinstall/reattach it to the list; the workflow items which were already in progress stops progressing further i.e. becomes dead, so each time when I modify workflow I remove the old workflow attach the new one and had to delete the entire old workflow items.

    So, is there a way to retain the workflow item in progress to use the new version of workflow when we reinstall the workflow in between. Please advice

  11. I have a question. I have created customized workflow using visual studio and have attached it to the SharePoint list. The problem I am getting is when I modify my workflow code and reinstall/reattach it to the list; the workflow items which were already in progress stops progressing further i.e. becomes dead, so each time when I modify workflow I remove the old workflow attach the new one and had to delete the entire old workflow items.

    So, is there a way to retain the workflow item in progress to use the new version of workflow when we reinstall the workflow in between. Please advice

  12. Today we welcome guest blogger Jim Steger , developer, blogger, and writer for Sonoma Partners with this

  13. Today we welcome guest blogger Jim Steger , developer, blogger, and writer for Sonoma Partners with this

  14. Today we welcome guest blogger Jim Steger , developer, blogger, and writer for Sonoma Partners with this

  15. Today we welcome guest blogger Jim Steger , developer, blogger, and writer for Sonoma Partners with this

  16. Joe Kelly says:

    MindenMan’s solution works great for emails coming from people who already have a contact record defined in CRM.

    When an email is received from a sender who has no contact setup in the CRM system then the step that updates the email (Step 5 above) fails with an error "invalid argument".

    Has anybody seen this and how is it resolved?

    TIA

    Joe

  17. Shaily says:

    I tried the code by MindenMan and faced a strange issue. The query that retrieves the contact with the specific emailaddress is not returning any record. The emailaddress being passed belongs to a contact but that contact record is not retrieved through query. The query works fine if used otherwise in a simple C# project. Does this have anything to do with the query being a part of the workflow activity. Has anybody else faced similar issue?

    Thanks

    Shaily

  18. Ashish says:

    Hi,

    I need to associate all the incoming mails to a fixed dummy account i.e. all the emails that are received in the queue has to be associated to one account only.

    How can this done?

    Thanks,

    Ashish

  19. Brett says:

    I’ve been working with Custom Workflow Activities recently. One workflow returns a lookup as output and the other returns a CrmBoolean as output.

    For either the lookup or the CrmBoolean, when I use the field to update a record, I get the following error:

    System.Workflow.Runtime.Hosting.PersistenceException: Type ‘System.Workflow.ComponentModel.ActivityExecutionContext’ in Assembly ‘System.Workflow.ComponentModel, Version=3.0.0.0, Culture=netural, PublicKeyToken=31bf3856ad364e35’ is not marked as serializable. at System.Workflow.Runtime.WorkflowExecutor.Persist(Activity dynamicActivity, Boolean unlock, Boolean needsCompensation) bla bla bla.

    Basically its a Serialization Exception whenever I use a non-native output type (i.e. Lookup or CrmBoolean). I would think that CRM would handle this since they are CRM types, but I don’t know what is wrong.

    The really weird thing is the record gets updated with the Output that I want, but the workflow fails because of the Serialization Exception. Any help would be greatly appreciated.

    Thanks,

    Brett

  20. Mark says:

    I also am having problems using this pattern and experience the same error that Brett posted above.

    I’m trying to expose a lookup as an output property that can then be used within a Send Email Activity. Like Brett, I receive a PersistenceException. Originally I was exposing my lookup as a pointer to a systemuser record. I wondered if the systemuser entity was somehow protected, so I have also tried to expose a lookup to an account record as in this sample. But I get the same error.

    Strangely, the custom workflow step seems to run correctly. I’ve attached a debugger and traced the code. When I incorporate this step in a workflow it always executes without error. But as soon as I add a Send Email Activity (even if it doesn’t reference the output parameter within the email content), the workflow fails on the Send Email step.

    My internet searches haven’t turned anything up. Unless I can discover why this won’t work I’m faced with having to re-develop a heap of workflows.

    Here’s the stack trace. Am happy to post the code if anyone thinks they can help.

    Thanks,

    Mark

    Workflow paused due to error: Unhandled Exception: System.Workflow.Runtime.Hosting.PersistenceException: Type ‘Microsoft.Crm.Asynchronous.SdkTypeProxyCrmServiceWrapper’ in Assembly ‘CrmAsyncService, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35’ is not marked as serializable.     at System.Workflow.Runtime.WorkflowExecutor.Persist(Activity dynamicActivity, Boolean unlock, Boolean needsCompensation)     at System.Workflow.Runtime.WorkflowExecutor.System.Workflow.ComponentModel.IWorkflowCoreRuntime.PersistInstanceState(Activity activity)     at System.Workflow.ComponentModel.Activity.MarkClosed()     at System.Workflow.ComponentModel.Activity.MarkCompleted()     at System.Workflow.ComponentModel.ActivityExecutionContext.CloseActivity()     at System.Workflow.ComponentModel.ActivityExecutorOperation.Run(IWorkflowCoreRuntime workflowCoreRuntime)     at System.Workflow.Runtime.Scheduler.Run()  Inner Exception: System.Runtime.Serialization.SerializationException: Type ‘Microsoft.Crm.Asynchronous.SdkTypeProxyCrmServiceWrapper’ in Assembly ‘CrmAsyncService, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35’ is not marked as serializable.     at System.Runtime.Serialization.FormatterServices.InternalGetSerializableMembers(RuntimeType type)     at System.Runtime.Serialization.FormatterServices.GetSerializableMembers(Type type, StreamingContext context)     at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitMemberInfo()     at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter)     at System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.Serialize(Object obj, ISurrogateSelector surrogateSelector, StreamingContext context, SerObjectInfoInit serObjectInfoInit, IFormatterConverter converter, ObjectWriter objectWriter)     at System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Object graph, Header[] inHeaders, __BinaryWriter serWriter, Boolean fCheck)     at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph, Header[] headers, Boolean fCheck)     at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(Stream serializationStream, Object graph)     at System.Workflow.ComponentModel.Activity.Save(Stream stream, IFormatter formatter)     at System.Workflow.ComponentModel.Activity.Save(Stream stream)     at System.Workflow.Runtime.Hosting.WorkflowPersistenceService.GetDefaultSerializedForm(Activity activity)     at Microsoft.Crm.Workflow.CrmWorkflowPersistenceService.SaveWorkflowInstanceState(Activity rootActivity, Boolean unlock)     at System.Workflow.Runtime.WorkflowExecutor.Persist(Activity dynamicActivity, Boolean unlock, Boolean needsCompensation)  

  21. Jeff says:

    @Brett and @Mark

    I ran into the same problem. In my case I had some public class variables that were not serializable (in fact, it was the CRMSdk object). Make these variables local to the function they are used in and then it won’t try to serialize them on the return call.

  22. Oliver says:

    I ran into this very same issue and, in my case, it was not a public variable.  It was actually a private variable I had declared!  I followed Jeff’s suggestion and the problem is gone.

    Thanks for the information.

    Cheers,

    Oliver

  23. Hi

    I ran into the same problem and found the solution to this problem.

    Just mark the fields in your custom workflow activity to [NonSerialized] and you won’t get this issue anymore.

    Regards

    Naim

Skip to main content