Part 2- Creating the user task activity

Frans van Ek
5 min readOct 12, 2022

--

This series describes the implementation of a User Interface driven by an Elsa workflow engine.

Several different steps will lead to a fully working implementation. This implementation might not give you the silver bullet for your specific needs. Still, it will provide insights into the various options.
In Part 1 — UI driven by Elsa Workflows the base solution setup was discussed. In this part, we will create the user task activity used throughout this series.

Photo by Ilya Pavlov on Unsplash

First, we add a project to the solution: UserTask.AddOns. It is a .Net6 class library.

This project will add all the needed elements for the user task activity. The application will have dependencies on three packages.

  • Elsa
  • Elsa.Server.Api
  • Microsoft.AspNetCore.Mvc.Core

Create the activity as a class with the name UserTaskSignal. This class will inherit from the Activity class.

The activity is a generic UI user task. However, different configurations of the task can result in any number of implementations and behaviors.
For example, a unique signal enables the engine to distinguish which user task needs to be resumed. The activity is a blocking activity that puts the execution of a workflow in a suspended mode until it is triggered to continue.

Be aware that other executions might continue when multiple branches are being executed in the workflow.

So, what makes this activity a blocking activity? The answer for that can be found in the OnExecute method, the activity tells the workflow engine that execution should pause. You can do that with Suspend();

protected override IActivityExecutionResult OnExecute()
{
return Suspend();
}

But since a blocking activity can be at the start of the workflow (if it is the first-time execution), it should not pause but assume the initial trigger is the UI that already handled the details and wants to start the workflow based upon the user’s input. (This scenario is handled in Part 6)
To mitigate this issue, the following code is used:

context
.WorkflowExecutionContext
.IsFirstPass ?
OnResume(context):
Suspend();

For resuming the activity, the code is pretty straightforward. The input is set as the output, so it can be easily used on the next activities. And the output is registered on the context.

Bookmarks

Now the handling of the bookmarks is in order.

The bookmark for the user task at this state is not using additional info for resuming the workflow. It just uses a signal to differentiate the different activations of the user tasks. Having other signals is vital to have, because a single workflow can have multiple user tasks running at the same time. Having different signals, helps to determine which branch in the workflow to resume. Besides that, the UI can use the signal to determine what kind of UI to show for handling the user task.

The bookmark implementation consists of the following elements:

  • IUserTaskSignalInvoker interface
  • UserTaskSignalInvoker
  • UserTaskSignalBookmark
  • UserTaskSignalBookmarkProvider

IUserTaskSignalInvoker interface is the interface for the UserTaskSignalInvoker. This implementation starts the appropriate workflows. Based upon the provided signal, the workflows are selected from the registered bookmarks and executed or dispatched.

The UserTaskSignalBookmarkProvider is the component that registers a bookmark when the activity enters the suspended mode. By using generics in the base implementation, the provider reports the bookmarks.
The UserTaskSignalBookmarkProvider creates a new UserTaskSignalBookmark instance that provides the bookmark’s details. During the workflow execution, the engine activates the provider.

For abbrevity, no code is displayed here. you can find it is the (Git repo)

All these components need to be registered in the engine. Therefore, an extension method is created to do just that.

The extension allows registering the components in the Startup.cs.
The EngineId is an additional option to allow expansion later on. For example, having multiple engines to handle user tasks but using only one UI to activate the workflows in the right Engine.

services.AddUserTaskSignalActivities(engineId)

Controller

The Engine needs to have an additional controller to enable the interaction between the UI application and the Engine.

The controller has the following endpoints

  • /{signalName} ==> Gets all workflows waiting for a specific signal to be returned
  • {signalName}/dispatch ==> Signals all workflows waiting on the specified signal name asynchronously.
  • {signalName}/execute ==> Signals all workflows waiting on the specified signal name synchronously.

For the endpoint to get all the workflow instances that are waiting for the signal is set to lowercase invariant. Something to keep in mind when choosing the signal names.

The bookmark filter is used to get the bookmarks associated with the signal. Another thing to remember is that the bookmark matching is done based on the Type name. This naming match might lead to issues when you have derived user tasks.

The other steps are pretty straight forward. Getting the workflow id, getting the workflow instances and convert them to a view model.

Activating the workflows (dispatch or execute) can have additional data to filter out the right workflow instances.
Be aware that you have to wrap the data in this envelope structure. So, the actual data for the user task is placed inside an input property.

{
"workflowInstanceId": "string",
"correlationId": "string",
"input": {}
}

Application

ProcessService is the component that starts new workflow instances.

UsertaskService is the component that sends the signal to the Api of the engine. The controller discussed in the previous paragraph is handling the requests of this service.

The UI itself has a button to create a workflow. The user task service fetches the workflow instances waiting for a user task signal to resume. And finally, the UI has a button for each workflow to continue when clicked.

Both services are registered in the Program.cs.

var baseAddress = builder.Configuration[“UsertaskService:BaseAddress”];// Add services to the container.builder.Services.AddRazorPages();builder.Services.AddServerSideBlazor();
builder.Services.AddScoped(sp =>
new UsertaskService(
new HttpClient { BaseAddress = new Uri(baseAddress)}));
builder.Services.AddScoped(sp =>
new ProcessService(
new HttpClient { BaseAddress = zew Uri(baseAddress)}));

In the next parts the handling of data is implemented.

Oh, btw all items not related to the user tasks are removed, like the counter page and the weather forecast. ;-) So, you won’t find them anymore in the repo.

Next Part 3: Adding functionality to return data from a user task

Used version: Elsa 2.10.467

--

--

Frans van Ek

Software developer architect with a passion for sustainability., just trying to improve the world a bit.