Custom Activities
This topic covers extending Elsa with your own custom activities.
Elsa includes many ready-made activities for various tasks, from simple ones like "Set Variable" to complex ones like "Send Email." These tools help build and
To unlock Elsa's full potential, create activities specific to your needs. Custom activities designed for your domain can improve workflow creation and management, making it more efficient and personalised.
Learn how to create custom activities to enhance Elsa's features, with easy steps to integrate these solutions into 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:
Let's dissect the sample PrintMessage
activity.
Essential Components
The
PrintMessage
class inherits fromElsa.Workflows.Activity
, which implements theIActivity
interface.The core of an activity is the
ExecuteAsync
method. It defines the action the activity performs when executed within a workflow.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
ExecuteAsync
is where the main action happens. For example,Console.WriteLine("Hello world!");
prints a message to the console. In real-world cases, this section would handle core tasks like data processing or connecting to other systems.Using
await context.CompleteActivityAsync();
means the activity is done. Completing an activity is key to moving the workflow.
Activity vs CodeActivity
If your custom activity has a simple workflow and ends right after finishing its task, using CodeActivity
makes things easier. This base class automatically marks the activity as complete once it's done, so you don't need to write any additional completion code.
Let's look at how to redo the PrintMessage
activity using CodeActivity
as the base. This highlights that manual completion isn't needed:
Metadata
The ActivityAttribute
can be used to give user-friendly details to your custom activity, such as its display name and description. Here's an example using ActivityAttribute
with the PrintMessage
activity. This is useful in tools like Elsa Studio.
In this example, the activity is annotated with a namespace of "MyCompany"
and a description for clarity.
Composition
Composite activities merge several tasks into one, enabling complex processes with conditions and branches. This is shown 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.
Programmatic Workflows and Dynamic Activities
There is an open issue reported on GitHub related to the Elsa Workflows project: Dynamically provided activities are not yet supported within programmatic workflows. You can view the issue here.
The following example shows how to use the If
activity:
Outcomes
Setting custom outcomes for activities gives precise control over what happens based on certain conditions. You can declare potential outcomes by using the FlowNodeAttribute
on the activity class. For example:
This attribute 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 sample activity:
In this example, 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 accepting arguments and returning results, activities can accept input and produce 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 on an activity, simply expose public properties within your activity class. For instance, the PrintMessage
activity below is updated to receive a message as input:
Metadata
Use the InputAttribute
to add input details to your custom activity, making it easy to include display names and descriptions. This feature improves the clarity of activity inputs, especially in tools like Elsa Studio.
Here is an instance where the InputAttribute
is applied to the Message
property:
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 use 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 used:
Output
Activities can generate outputs. To do so, implement properties typed as Output<T>
.
For instance, the activity below generates a random number between 0 and 100:
Metadata
Like input properties, output properties can be enriched with metadata.
This is done using the OutputAttribute
.
An example of the OutputAttribute
applied to the Result
property follows:
Workflow users have two approaches to using 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
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
Direct Access
And here's how to access to the output from the GenerateRandomNumber
activity directly:
This approach 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:
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.
Dependency Injection
To use services in your activities, you can get them using the context
in the activity's ExecuteAsync
method. This allows for easy use of dependency injection in workflows.
Here's a simple example of how to use a service in an activity:
Choosing Service Location over Constructor Injection
Elsa prefers to use service location over constructor dependency injection to make it easier to create activity instances in workflow definitions. Using constructor-based DI would make it harder to build and change workflow graphs programmatically.
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. Examples of blocking activities include the Event
and Delay
activities.
Here is an example of a blocking activity that creates a bookmark to pause its execution, awaiting an external trigger to proceed:
Here's how to add the MyEvent
activity to a workflow:
When the workflow starts, it will run until it reaches the MyEvent
activity. At this point, the workflow will pause, create a bookmark, and wait for an external signal to continue. While waiting, there is nothing else for the workflow engine to do, so it will save the workflow instance and remove it from memory.
To pick up a workflow from a bookmark, the system needs certain information:
The type of activity that initiated the bookmark
The bookmark payload, which was generated by the activity
How to Resume a Workflow Using IWorkflowRuntime
:
Follow these steps to restart a workflow using the bookmark payload from the blocking activity, by using IWorkflowRuntime
:
This method can be easily added to an API controller to resume workflows when external events happen.
Triggers
Triggers serve as specialised 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 using the ITrigger
interface or inheriting from Trigger
, an activity becomes a trigger. This allows services like IWorkflowRuntime
to start workflows based on certain events, which is key for creating reactive and event-driven workflows.
Understanding trigger activities in workflows is key. These activities check if they directly started the current execution. If they did, they finish immediately instead of pausing the workflow for an external trigger. This prevents the workflow from stopping at the start and ensures it only pauses when needed later.
By using this mechanism, workflows start automatically when events like the MyEvent
activity happen. Here's an example of a workflow
Set the CanStartWorkflow
property to true
on the trigger activity. This allows the activity to start the workflow, making it essential for activating workflow triggers.
To programmatically trigger workflows using the MyEvent
trigger, apply the same code used for resuming a bookmark with IWorkflowRuntime
.
The IWorkflowRuntime
service is a helpful service for handling workflow processes, like starting new workflows or resuming suspended ones. It allows developers to easily create workflows that adapt to different events, improving how interactive and responsive their applications are.
Registering Activities
Register activities in the Activity Registry before using them in workflows.
The easiest way to register activities is through your application's startup code. For example, 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 used:
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.
Activity Providers
Activities can be provided to the system in various ways. The type of an activity is fundamentally represented by an Activity Descriptor.
Activity Descriptors are provided by Activity Providers. Out of the box, Elsa ships with one such 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.
Consider a scenario where you generate activities from an Open API specification. Each resource operation is automatically represented as an activity, rather than using the SendHttpRequest
activity directly.
To develop custom activity providers, follow these steps:
Implement the
Elsa.Workflows.Contracts.IActivityProvider
.Register your custom activity provider with the system.
Below is a sample implementation of an activity provider that dynamically produces activities based on a list of fruits.
This provider leverages a simple array of fruit names as its source, generating an activity descriptor for each fruit, symbolising a "Buy (fruit)" activity.
To register this provider, utilise the AddActivityProvider<T>
extension method:
Programmatic Workflows and Dynamic Activities
Currently, dynamically provided activities cannot be used within programmatic workflows.
An open issue exists for this functionality: https://github.com/elsa-workflows/elsa-core/issues/5162
Summary
In this topic, we explored the creation, registration, and utilisation of custom activities. These are crucial in workflow development as they allow for the inclusion of domain-specific actions within a workflow.
Last updated