Custom Activities
Elsa is equipped with a comprehensive suite of prebuilt activities, designed to support a wide range of use cases, from basic operations like "Set Variable" to more advanced functions such as "Send Email." These out-of-the-box activities lay a solid foundation for efficiently constructing and managing workflows across a multitude of scenarios.
Yet, to fully unlock Elsa's potential, it's highly recommended to craft domain-specific activities that cater to your unique requirements. By developing custom activities with your specific domain in mind, you can significantly enhance the workflow creation and management process, making it more streamlined and tailored to your operational needs.
This guide will explore the creation of custom activities, providing insights and actionable steps on how to augment Elsa's functionality with custom solutions that seamlessly integrate with your domain.
Creating Custom Activities
To create a custom activity, start by defining a new class that implements the IActivity
interface or inherits from a base class that does. Examples include Activity
or CodeActivity
.
A simple example of a custom activity is one that outputs a message to the console:
Anatomy
When developing custom activities for Elsa, understanding the basic structure and lifecycle of an activity is crucial.
Let's dissect the sample PrintMessage
activity.
Essential Components
Inheritance: The
PrintMessage
class inherits fromElsa.Workflows.Activity
, which implements theIActivity
interface.Execution Method: The core of an activity is the
ExecuteAsync
method. It defines the action the activity performs when executed within a workflow.Activity Execution Context: The
ActivityExecutionContext
parameter, namedcontext
here, provides access to the workflow's execution context. It's a gateway to the workflow's environment, offering methods to interact with the workflow's execution flow, data, and more.
Key Operations
Performing the Activity's Task: Inside
ExecuteAsync
, the activity performs its intended function. In this example,Console.WriteLine("Hello world!");
demonstrates a simple operation of printing a message to the console. In real-world scenarios, this is where the activity's primary logic would reside, whether it's processing data or integrating with external systems.Completing the Activity: The call to
await context.CompleteActivityAsync();
signifies the completion of the activity's execution. Completing an activity is a critical step in progressing the workflow to its next stage or activity.
Activity vs CodeActivity
When your custom activity's workflow is straightforward and concludes immediately after its task, inheriting from CodeActivity
offers a streamlined approach. This base class is engineered to automatically signal the completion of the activity post-execution, eliminating the need for explicit completion logic.
To illustrate, let's revisit the PrintMessage
activity, this time reimagined using CodeActivity
as its base. This example underscores the absence of manual completion:
This approach allows developers to focus on the core logic of their activities without worrying about activity completion.
Metadata
When utilizing activities within tools such as Elsa Studio, it presents an opportunity to convey user-friendly details about the activity, like its display name and description.
Such information can be associated with your custom activity through the ActivityAttribute
.
Below is an example where the ActivityAttribute
is applied to the PrintMessage
activity:
In this case, the activity is annotated with a namespace of "MyCompany"
and a description for clarity.
Composition
Composite activities combine multiple activities into a single unit, allowing for complex workflows that include conditional logic and branching. This capability is demonstrated in the If
activity example below:
This example illustrates how a composite activity can evaluate a condition and then proceed with one of two possible paths, effectively modeling an "if-else" statement within a workflow.
The following example shows how to use the If
activity:
When adding the activity to the canvas, notice that the two properties are presented as embedded ports inside the activity:
To add activities to an embedded port, simply click on one of them, which will take you to a nested designer:
Outcomes
Defining custom outcomes for an activity allows for nuanced control over its execution path based on specific conditions. These outcomes can be explicitly declared by annotating the activity class with the FlowNodeAttribute
. For example:
This annotation specifies two distinct outcomes for the activity: "Pass" and "Fail." These outcomes dictate the possible execution paths following the activity's completion. To trigger a specific outcome during runtime, utilize the CompleteActivityWithOutcomesAsync
method within your activity's execution logic.
Consider the following practical illustration:
In this scenario, the defined outcomes guide the flow of execution within flowcharts, enabling conditional progression based on the result of the activity. This mechanism enhances the flexibility and decision-making capabilities within workflows, allowing for dynamic responses to activity results.
Input
Similar to C# methods and JavaScript functions, activities can accept input and generate output.
In essence, an activity functions within a workflow much like a statement within a program, serving as a fundamental component that constructs the logic of the workflow.
To define inputs, simply expose public properties within your activity class. For instance, the PrintMessage
activity below is updated to receive a message as input:
Input Metadata
Utilizing activities within tools such as Elsa Studio, offers a chance to provide accessible information about the activity's inputs, including display names and descriptions.
This detail can be appended to your custom activity's input property using the InputAttribute
.
Here is an instance where the InputAttribute
is applied to the Message
property:
In this example, the Message
input property is embellished with a description for better understanding.
Expressions
Often, you'll want to dynamically set the activity's input through expressions, instead of fixed, literal values.
For instance, you might want the message to be printed to originate from a workflow variable, rather than being hardcoded into the activity's input.
To enable this, you should encapsulate the input property type within Input<T>
.
As an illustration, the PrintMessage
activity below is modified to support expressions for its Message
input property:
Note that encapsulating an input property with Input<T>
changes the manner in which its value is accessed:
The example below demonstrates specifying an expression for the Message
property in a workflow created using the workflow builder API:
In this scenario, we utilize a simple C# delegate expression to dynamically determine the message to print at runtime.
Alternatively, other installed expression provider syntaxes, such as JavaScript, can be employed:
The only variance is in the creation of the expression:
When an input property is encapsulated with Input<T>
, Elsa Studio enables the user to select from any installed syntaxes for input:
Output
Activities are capable of generating outputs, achieved by implementing properties typed as Output<T>
.
For instance, the activity below generates a random number between 0 and 100:
Output Metadata
Similarly to input properties, output properties can be enriched with metadata.
This is accomplished using the OutputAttribute
.
An example of the OutputAttribute
applied to the Result
property follows:
In this instance, the Result
output property is adorned with a description for enhanced clarity.
There are two approaches to managing activity output:
Capturing the output via a workflow variable
Direct access to the output from the workflow engine's memory register
Let's examine both methods in detail.
Capture via Variable
Firstly, here's how to capture the output using a workflow variable:
In this workflow, the steps include:
Executing the
GenerateRandomNumber
activityCapturing the activity's output in a variable named
RandomNumber
Displaying a message with the value of the
RandomNumber
variable
To capture output via a variable, follow these steps:
Create a new workflow
Establish a new variable of type
decimal
named RandomNumberIntegrate the
activity into the canvas and link its Result output to the variable.Add the
activity to the canvas and set up its property with a JavaScript expression leveraging the variable.Ensure the activities are connected.
Direct Access
Now, let's explore direct access to the output from the GenerateRandomNumber
activity:
This requires naming the activity from which the output will be accessed, as well as the output property's name.
An alternative, type-safe method is to declare the activity as a local variable initially. This allows for referencing both the activity and its output, as demonstrated below:
To directly access the output from the
activity, follow these steps:Remove the RandomNumber variable
Upon adding an activity to the canvas, the designer automatically assigns it a default name. Use the name
GenerateRandomNumber1
for the activity as an example.Knowing the activity's name, update the
activity's property with the following expression to access its output:const randomNumber = getResultFromGenerateRandomNumber1(); return `The random number is: ${randomNumber}`;var randomNumber = Output.From<decimal>("GenerateRandomNumber1", "Result"); return $"The random number is: {randomNumber}";
While both approaches are effective for managing activity output, it's crucial to note a key distinction: activity output is transient, existing only for the duration of the current execution burst.
To access the output value beyond these bursts, capturing the output in a variable is recommended, as variables are inherently persistent.
Leveraging Dependency Injection in Activities
To integrate external services within your activities, you can dynamically resolve these services using the context
argument provided in the activity's ExecuteAsync
method. This approach allows for the seamless use of dependency injection (DI) patterns, even within the constraints of workflow execution.
Below is a practical example demonstrating how to resolve a service in an activity:
Blocking Activities
Blocking activities represent an important concept in workflow design, enabling a workflow to pause its execution until a specified external event occurs. Instead of completing immediately, these activities generate a bookmark—a placeholder of sorts—that allows the workflow to resume from the same point once the required conditions are met. This mechanism is particularly useful for orchestrating asynchronous operations or waiting for external inputs. Notable examples of blocking activities include the Event
and Delay
activities.
Here is an illustrative example of a blocking activity that creates a bookmark to pause its execution, awaiting an external trigger to proceed:
Below, we demonstrate how to incorporate the MyEvent
activity into a workflow:
Upon initiating the workflow, execution will proceed until it reaches the MyEvent
activity. At this juncture, the activity will pause the workflow by not completing and instead creating a bookmark, waiting for an external signal to continue. While waiting for the external signal to be received, there is no more work for the workflow engine to do and will persist the workflow instance and remove it from memory.
To effectively resume a workflow from a bookmark, the workflow runtime requires specific information:
The type of activity that initiated the bookmark
The bookmark payload, which was generated by the activity
Here's how to programmatically resume a workflow, utilizing the bookmark payload produced by the blocking activity, with the IWorkflowRuntime
service:
This approach could be seamlessly integrated into an API controller to facilitate the resumption of workflows in response to external events.
Triggers
Triggers serve as specialized activities designed to initiate workflows in reaction to specific external events, such as HTTP requests or messages from a message queue. This capability allows workflows to dynamically respond to outside stimuli, making them highly versatile in various automated processes.
To illustrate, the MyEvent
activity, previously discussed as a blocking activity, can also be adapted to function as a trigger:
By implementing the ITrigger
interface, or indirectly by inheriting from Trigger
as is seen in this example, the activity becomes a trigger, thereby enabling higher-level services such as IWorkflowRuntime
to start workflows in response to predefined events. This mechanism is integral to the design of reactive, event-driven workflows.
It's important to understand the nuanced behavior of trigger activities within the workflow's execution. Specifically, these activities are designed to assess whether their activation directly initiated the current execution burst. If so, instead of generating a bookmark—which would ordinarily pause the workflow awaiting an external trigger—they opt to complete immediately. This approach prevents the workflow from entering a paused state right at the start, ensuring that it only pauses when waiting for subsequent triggers.
With this mechanism in place, workflows can be dynamically initiated by events associated with trigger activities, such as the MyEvent
activity. Let's explore an example workflow that leverages this capability:
In this instance, it's crucial to set the CanStartWorkflow
property to true
on the trigger activity. This configuration signals that the activity has the authority to commence the workflow, serving as a key component in activating workflow triggers.
In the visual designer, achieving this activation entails selecting the
option. This step is essential to empower the activity with the capability to automatically initiate the workflow.To programmatically trigger any and all workflows with the MyEvent
trigger, we use the exact same code as we did when resuming a bookmark using the IWorkflowRuntime
service:
This streamlined approach underscores the versatility of the IWorkflowRuntime
service in managing workflow executions, whether it's initiating new workflows or resuming paused ones through bookmarks. By leveraging a consistent method, developers can efficiently design workflows that respond dynamically to both internal and external events, further enhancing the interactivity and responsiveness of their applications.
Registering Activities
For activities to be usable within workflows, they must first be registered within the system.
This involves registering them with a service known as the Activity Registry.
The most straightforward method is through your application's initialization code. For instance, the Program.cs file below illustrates how to register the PrintMessage
activity:
Alternatively, to register all activities from a specific assembly, the AddActivitiesFrom<TMarker>
extension method can be utilized:
This approach registers all activities discovered within the assembly containing the specified type. The marker type can be any class within the assembly, not necessarily an activity. It serves as a pointer to the assembly to be searched for activity types.
Activity Providers
Activities can be supplied to the system in various ways, fundamentally represented by an Activity Descriptor.
Such descriptors are furnished by a construct known as Activity Providers, with one implementation being the TypedActivityProvider
. This provider generates activity descriptors based on the .NET types implementing the IActivity
interface.
This abstraction layer enables sophisticated scenarios where activity descriptors' sources can be dynamic.
For instance, Elsa facilitates the definition of workflows that can then be executed as activities. Via a specialized activity provider, these workflows manifest as toolbox-available activities.
An additional use case involves generating activities from an Open API specification, automatically representing each resource operation as an activity instead of directly utilizing the SendHttpRequest
activity.
To develop custom activity providers, adhere to the following steps:
Implement the
Elsa.Workflows.Contracts.IActivityProvider
Register your custom activity provider with the system
Below is a sample implementation of an activity provider:
This provider leverages a simple array of fruit names as its source, generating an activity descriptor for each fruit, symbolizing a "Buy (fruit)" activity.
To register this provider, utilize the AddActivityProvider<T>
extension method:
Summary
This exploration has equipped us with the knowledge to enhance Elsa through the integration of custom activities.
Creating a custom activity entails developing a new class that either directly or indirectly implements the IActivity
interface.