Scheduling Workflows in Dynamics CRM

There is no way out-of-the-box to schedule workflows to run in Dynamics CRM. To get round this, you can schedule Bulk Delete jobs, and trigger a process when the records affected by those jobs are deleted.

I took inspiration from this blog post by Gus Gonzalez and implemented a generic task scheduling mechanism using Bulk Delete. I’ve done it a bit differently from how it’s described in that post, but the principle is the same.

The implementation involves a custom Entity (called Scheduled Task) and two plugins. One plugin runs when a Scheduled Task record is deleted, the other when one is created.

Scheduled Task Entity

The Scheduled Task entity stores:

  • The name of the workflow you want to run (this workflow is created against the Scheduled Task entity*)
  • The time it should next be run (they are run on the hour)
  • The frequency (the number of hours until it is run again)

Scheduled Task Entity Form in CRM 2011

Because each Scheduled Task record contains the name of the workflow to run, you can create as many Scheduled Task records as you like, each corresponding to a different workflow, perhaps scheduled at different frequencies.

Plugins

There are two plugins registered against the Scheduled Task Delete and Create steps (both pre-operation).

Delete Plugin

The Delete Plugin runs when a Scheduled Task record is deleted (typically by a Bulk Delete job). It performs these steps:

  1. Check that the Scheduled Task record is Active, and if not, skip steps 2 and 3. This is a simple way to allow users to disable the functionality, and/or to genuinely delete an unwanted record.
  2. Read the values from the record being deleted, and use them to create a new Scheduled Task record with an updated run time. For example, if the run time of the record being deleted is midnight, and the frequency is 3 hours, the next one created will have a run time of 03.00.
  3. Run the workflow against the newly-created Scheduled Task record*.

*If you don’t like the Workflow being defined against the Scheduled Task Entity, an alternative would be to add an extra field to hold Fetch XML. The Delete Plugin could run that query and fire a Workflow against each record that is returned, rather than firing the workflow against the Scheduled Task.

Create Plugin

The Create Plugin runs when a Scheduled Task record is created (by Step 2 of the Delete Plugin or manually). It performs these steps:

  1. Give the new record a friendly name: "Run workflow '{0}' on or after {1:dd MMM yyyy HH:mm}"
  2. Delete any completed asyncoperation records created by this process that are older than one week.
  3. Ensure that the Bulk Delete jobs exist, and if they don’t, create them.

Code Examples

I haven’t included all of the code to reproduce this, just a couple of bits which it might not be immediately obvious how to do without looking up. If you are trying to implement something similar and want to know how I did something that’s not detailed here, please leave a comment below.

How to Schedule Bulk Delete Jobs Programmatically

Bulk Delete jobs can only be scheduled daily, so if you want your scheduled tasks to be able to run hourly, you will need 24 of them. Here’s the code for creating these:

// This query selects Scheduled Task entities with "Run on or after" date in the past.
// This tells the Bulk Delete job what to delete.
QueryExpression qe = new QueryExpression("name_of_your_scheduled_task_entity");
qe.Criteria.AddCondition(new ConditionExpression("name_of_your_date_field", ConditionOperator.LastXYears, 10));
qe.Criteria.AddCondition(new ConditionExpression("statecode", ConditionOperator.Equal, 0));

// Work out start time of first job (top of the next hour)
DateTime startDateTime = new DateTime(DateTime.Today.Year, DateTime.Today.Month, DateTime.Today.Day, DateTime.Today.Hour, 0, 0).AddHours(1);

//  Create 24 jobs, one per hour
for (int i = 1; i <= 24; i++)
{
    BulkDeleteRequest bulkDeleteRequest = new BulkDeleteRequest
    {
        JobName = string.Format("[{0:HH:mm}] {1}", startDateTime, "Scheduled Task Bulk Delete"),
        RecurrencePattern = "FREQ=DAILY;INTERVAL=1;",
        StartDateTime = startDateTime,
        SendEmailNotification = false,
        ToRecipients = new Guid[] {},
        CCRecipients = new Guid[] {},
        QuerySet = new[] {qe}
    };

    orgService.Execute(bulkDeleteRequest);

    startDateTime = startDateTime.AddHours(1);
}

Running a Workflow Programmatically (from the Delete Plugin)

This function gets the Id of a Workflow. You supply the workflow name and the Entity type (logical name):

public static Guid GetWorkflowId(IOrganizationService orgService, string entityType,
string workflowName)
{
    QueryExpression query = new QueryExpression("workflow");
    query.Criteria.AddCondition("name", ConditionOperator.Equal, workflowName);
    query.Criteria.AddCondition("type", ConditionOperator.Equal, 1);
    query.Criteria.AddCondition("ondemand", ConditionOperator.Equal, true);
    query.Criteria.AddCondition("primaryentity", ConditionOperator.Equal, entityType);

    Entity workflow = RetrieveSingleEntity(orgService, query);
    return (workflow == null) Guid.Empty : workflow.Id;
}

This method runs a workflow against a specific Entity. Use the above method to get the Workflow Id.

public static ExecuteWorkflowResponse RunWorkflow(IOrganizationService orgService, Guid entityId, Guid workflowId)
{
    ExecuteWorkflowRequest wfExecute = new ExecuteWorkflowRequest
    {
        WorkflowId = workflowId,
        EntityId = entityId
    };
    return (ExecuteWorkflowResponse)orgService.Execute(wfExecute);
}

This is a general function for returning a single entity from a Query. I’ve included this as it is called by GetWorkflowId.

private static Entity RetrieveSingleEntity(IOrganizationService orgService, QueryExpression query)
{
    query.PageInfo = new PagingInfo { Count = 1, PageNumber = 1 };
    EntityCollection results = orgService.RetrieveMultiple(query);
    return (results.Entities == null || results.Entities.Count < 1) ? null : results.Entities[0];
}

Real World Use

So far, I’ve used this on two occasions. One was to run an archiving process that looked for records of a certain age, copied a subset of the information into a new record of a different Entity type, and deleted the original. The second implementation updated an urgency value on each open Case based on time parameters defined in a Service Level Agreement Entity to keep a dashboard up to date through the day. More uses to come, no doubt.

1 Response

  1. Ciar Baharie 16 March 2017 / 11.49

    Needed to make a call every 5 minutes. Used this method to trigger a workflow every hour that then ran 12 staggered calls. Worked great and the only viable work around I could find for hosted solutions.

    Thank you very much for your blog post.

Leave a Reply

Your email address will not be published. Required fields are marked *