Custom Activities
Complete guide to extending Elsa Workflows V3 with custom activities, including inputs/outputs, blocking activities, triggers, dependency injection, and UI hints.
Elsa Workflows includes a rich library of built-in activities for common tasks, from simple operations like "Set Variable" to complex ones like "Send Email" and "HTTP Request." While these activities cover many scenarios, the true power of Elsa lies in creating custom activities tailored to your specific domain and business requirements.
Custom activities allow you to:
Encapsulate domain-specific business logic
Integrate with external systems and APIs
Create reusable workflow building blocks
Provide a better experience for workflow designers
This guide covers everything you need to know about creating custom activities in Elsa V3, from basic examples to advanced patterns including blocking activities and triggers.
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:
using Elsa.Extensions;
using Elsa.Workflows;
public class PrintMessage : Activity
{
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
Console.WriteLine("Hello world!");
await context.CompleteActivityAsync();
}
}Let's dissect the sample PrintMessage activity.
Essential Components
The
PrintMessageclass inherits fromElsa.Workflows.Activity, which implements theIActivityinterface.The core of an activity is the
ExecuteAsyncmethod. It defines the action the activity performs when executed within a workflow.The
ActivityExecutionContextparameter, namedcontexthere, 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
ExecuteAsyncis 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" , a category of "MyPlatform/MyFunctions" and a description for clarity.
The Treeview activity picker in Elsa Studio supports nested categories within the tree. Simply use the / character to separate categories. More detials can be found here.
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.
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
Activities can accept inputs, similar to how C# methods accept parameters. This allows workflow designers to configure activity behavior at design time or dynamically at runtime.
Basic Input Properties
To define inputs on an activity, expose public properties within your activity class. For instance, the PrintMessage activity below accepts a message as input:
Input Metadata
Use the InputAttribute to provide metadata about your inputs. This metadata is used by Elsa Studio to provide a better user experience when configuring activities:
The InputAttribute supports several properties:
DisplayName: The name shown in the designer (defaults to property name)
Description: Help text shown to users
Category: Groups related inputs together
DefaultValue: Default value when the activity is added
Options: For dropdown/radio lists
UIHint: Controls the input editor type (see UI Hints section below)
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:
UI Hints
UI Hints control how input properties are displayed and edited in Elsa Studio. By specifying a UI hint, you can provide a better user experience for workflow designers by using appropriate input controls.
Available UI Hints
Elsa Studio provides several built-in UI hints through the InputUIHints class:
SingleLine: Single-line text input (default for strings)
MultiLine: Multi-line text area
Checkbox: Boolean checkbox
CheckList: Multiple selection checklist
RadioList: Single selection radio button list
DropDown: Dropdown select list
CodeEditor: Code editor with syntax highlighting
JsonEditor: JSON-specific editor with validation
DateTimePicker: Date and time picker
VariablePicker: Select from available workflow variables
OutputPicker: Select from activity outputs
OutcomePicker: Select from activity outcomes
TypePicker: Select a .NET type
WorkflowDefinitionPicker: Select a workflow definition
Using UI Hints
Specify the UI hint using the UIHint property of the InputAttribute:
Multi-Select Inputs
For inputs that allow multiple selections, use CheckList with a collection type:
Date and Time Inputs
For date and time values, use the DateTimePicker hint:
Variable and Output Pickers
To help users select from available workflow variables or activity outputs:
Output
Activities can generate outputs that can be used by subsequent activities in the workflow. To define outputs, use properties typed as Output<T>.
Basic Output Properties
Here's an example of an activity that generates a random number:
Output Metadata
Use the OutputAttribute to provide metadata about your outputs:
Multiple Outputs
Activities can have multiple output properties:
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
GenerateRandomNumberactivityCapturing the activity's output in a variable named
RandomNumberDisplaying a message with the value of the
RandomNumbervariable
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
Activities can access services registered in the dependency injection container using the ActivityExecutionContext. This enables activities to integrate with external systems, databases, APIs, and other application services.
Service Location Pattern
Retrieve services using the GetRequiredService<T>() or GetService<T>() methods on the context:
Multiple Service Dependencies
Activities can access multiple services as needed:
Optional Services
Use GetService<T>() instead of GetRequiredService<T>() when a service is optional:
Blocking Activities
Blocking activities are a powerful concept in Elsa that enable workflows to pause execution and wait for external events or conditions. Instead of completing immediately, these activities create a bookmark—a persistence point that allows the workflow to be saved and resumed later when the required event occurs.
This mechanism is essential for:
Long-running workflows that span hours, days, or longer
Waiting for user input or approval
Waiting for external system responses
Coordinating with other workflows or processes
Creating a Basic Blocking Activity
Here's a simple example of a blocking activity that waits for an external event:
Using Blocking Activities in Workflows
Here's how to use a blocking activity in a workflow:
When this workflow executes:
It prints "Workflow started - waiting for approval..."
It reaches the
WaitForEventactivity and creates a bookmarkThe workflow is persisted and removed from memory
The workflow waits until an external system resumes it
When resumed, it prints "Approval received - continuing workflow!"
Resuming Blocked Workflows
To resume a workflow that's waiting at a bookmark, use the IStimulusSender service:
Bookmarks with Payloads
Bookmarks can carry data that's available when the workflow resumes:
Resuming with Data
When resuming a workflow with data:
Blocking Activities Best Practices
Unique Bookmarks: Ensure bookmark payloads are unique enough to identify the correct workflow instance
Timeout Handling: Consider implementing timeout mechanisms for long-running waits
Idempotency: Design resume handlers to be idempotent in case of duplicate resume calls
Error Handling: Implement proper error handling in resume callbacks
Testing: Test both the blocking and resume paths thoroughly
Triggers
Triggers are specialized activities that can both start new workflow instances and resume suspended workflows. They respond to external events such as HTTP requests, timer events, message queue messages, or custom application events.
A trigger activity differs from a regular blocking activity in that it:
Can automatically start new workflow instances when events occur
Implements the
ITriggerinterface or inherits from theTriggerbase classProvides a trigger payload for workflow discovery and matching
Creating a Trigger Activity
Here's how to create a trigger that can both start and resume workflows:
Key Trigger Concepts
IsTriggerOfWorkflow(): This method checks if the trigger activity started the current workflow execution. When true, the activity should complete immediately rather than creating a bookmark.
GetTriggerPayload(): This method returns a value that's used to match incoming events to workflow instances. The workflow runtime uses this payload to determine which workflows should be started when an event occurs.
CanStartWorkflow: This property on the activity instance must be set to true to allow the trigger to start new workflows.
Using Triggers in Workflows
Configure a workflow to be triggered by an event:
Triggering Workflows
To trigger workflows from your application code:
Advanced Trigger Example: Webhook
Here's a more sophisticated trigger example for webhooks:
Trigger Best Practices
Always check IsTriggerOfWorkflow(): Ensure triggers complete immediately when starting workflows
Set CanStartWorkflow = true: Required for triggers to actually start new workflow instances
Unique Payloads: Use unique trigger payloads to correctly match events to workflows
Handle Both Modes: Design triggers to work both as workflow starters and as blocking activities
Include Resume Callbacks: Always provide a resume callback when creating bookmarks
Pass Data Forward: Ensure event data flows through to subsequent activities via outputs
Registering Activities
Before custom activities can be used in workflows, they must be registered with Elsa's activity registry. There are several ways to register activities depending on your needs.
Register Individual Activities
To register specific activity types, use the AddActivity<T>() method:
Register Activities from Assembly
To register all activities from a specific assembly automatically:
This discovers and registers all types implementing IActivity in the specified assembly. The type parameter can be any type in the target assembly—it serves as a marker to identify the assembly.
Register Activities from Multiple Assemblies
For larger applications with activities spread across multiple assemblies:
Register Activities with Assembly Scanning
For advanced scenarios, you can scan assemblies dynamically:
Register Activity Dependencies
If your activities depend on services, register those services as well:
Complete Registration Example
Here's a complete example showing activity registration in Program.cs:
Verify Activity Registration
To verify that your activities are registered correctly, you can inject IActivityRegistry and check:
Activity Providers
Activity Providers enable advanced scenarios where activities are generated dynamically at runtime rather than being statically defined as .NET types. This powerful abstraction allows you to create activities from external sources such as APIs, databases, or configuration files.
Understanding Activity Descriptors
In Elsa, activities are represented by Activity Descriptors, which contain metadata about an activity including its name, category, inputs, outputs, and how to construct instances.
By default, Elsa uses the TypedActivityProvider which creates descriptors from .NET types implementing IActivity. However, you can create custom providers to generate activities from any source.
Use Cases for Custom Activity Providers
API Integration: Generate activities from OpenAPI/Swagger specifications
Database-Driven: Load activity definitions from a database
Dynamic Configuration: Create activities based on configuration files
Multi-Tenancy: Provide different activities for different tenants
Plugin Systems: Load activities from external plugins or modules
Creating a Custom Activity Provider
Here's an example that generates activities dynamically from a simple list:
Advanced Example: OpenAPI Activity Provider
Here's a more sophisticated example that could generate activities from an OpenAPI specification:
Registering Activity Providers
Register your custom activity provider in Program.cs:
Activity Provider Best Practices
Cache Descriptors: Consider caching activity descriptors to avoid regenerating them on every request
Async Loading: Use async/await for loading activity definitions from external sources
Error Handling: Implement proper error handling for external data sources
Unique Type Names: Ensure generated type names are unique and stable
Performance: Be mindful of performance when generating large numbers of activities
Summary
Custom activities are the foundation of extending Elsa Workflows to meet your specific business needs. This guide covered everything you need to create powerful, reusable workflow activities in Elsa V3:
What You Learned
Basic Activities: Creating simple activities using
ActivityandCodeActivitybase classesInputs and Outputs: Defining activity parameters with
Input<T>andOutput<T>, including metadata with attributesUI Hints: Controlling how inputs are displayed in Elsa Studio with various UI hint options
Expressions: Supporting dynamic values through C#, JavaScript, and other expression providers
Metadata: Using
ActivityAttribute,InputAttribute, andOutputAttributeto enhance the designer experienceComposite Activities: Building complex activities that compose other activities
Custom Outcomes: Defining multiple execution paths using the
FlowNodeattributeDependency Injection: Accessing services through the activity execution context
Blocking Activities: Creating activities that pause workflow execution until external events occur
Triggers: Building activities that can both start new workflows and resume existing ones
Registration: Various patterns for registering activities with Elsa
Activity Providers: Dynamically generating activities from external sources
Next Steps
Now that you understand custom activities, consider:
Explore Built-in Activities: Study Elsa's built-in activities for patterns and best practices
Create Domain Activities: Build activity libraries specific to your business domain
Share Activities: Package your activities as NuGet packages for reuse across projects
Advanced Topics: Learn about custom UI components, field extensions, and Studio customization
Additional Resources
Elsa Core Source Code - Reference implementation of built-in activities
UI Hints Documentation - Complete guide to UI hints
Reusable Triggers - Advanced trigger patterns (v3.5+)
Logging Framework - Integrate logging in your activities
Custom activities unlock the full potential of Elsa Workflows, enabling you to create sophisticated, domain-specific workflow solutions that perfectly fit your requirements.
Last updated