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 PrintMessage class inherits from Elsa.Workflows.Activity, which implements the IActivity 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, named context 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" , 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.

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

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:

  1. Capturing the output via a workflow variable.

  2. 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 activity

  • Capturing 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

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:

Why Service Location?

Elsa uses the service location pattern instead of constructor injection to simplify activity instantiation in workflow definitions. This design choice makes it easier to create and configure activities programmatically without needing to provide constructor parameters.

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:

  1. It prints "Workflow started - waiting for approval..."

  2. It reaches the WaitForEvent activity and creates a bookmark

  3. The workflow is persisted and removed from memory

  4. The workflow waits until an external system resumes it

  5. 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

  1. Unique Bookmarks: Ensure bookmark payloads are unique enough to identify the correct workflow instance

  2. Timeout Handling: Consider implementing timeout mechanisms for long-running waits

  3. Idempotency: Design resume handlers to be idempotent in case of duplicate resume calls

  4. Error Handling: Implement proper error handling in resume callbacks

  5. 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 ITrigger interface or inherits from the Trigger base class

  • Provides 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

  1. Always check IsTriggerOfWorkflow(): Ensure triggers complete immediately when starting workflows

  2. Set CanStartWorkflow = true: Required for triggers to actually start new workflow instances

  3. Unique Payloads: Use unique trigger payloads to correctly match events to workflows

  4. Handle Both Modes: Design triggers to work both as workflow starters and as blocking activities

  5. Include Resume Callbacks: Always provide a resume callback when creating bookmarks

  6. 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:

Programmatic Workflows and Dynamic Activities

Currently, dynamically provided activities cannot be used within programmatic workflows defined in C#. They are only available in workflows created through Elsa Studio or JSON definitions.

An open issue exists for this functionality: https://github.com/elsa-workflows/elsa-core/issues/5162

Activity Provider Best Practices

  1. Cache Descriptors: Consider caching activity descriptors to avoid regenerating them on every request

  2. Async Loading: Use async/await for loading activity definitions from external sources

  3. Error Handling: Implement proper error handling for external data sources

  4. Unique Type Names: Ensure generated type names are unique and stable

  5. 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 Activity and CodeActivity base classes

  • Inputs and Outputs: Defining activity parameters with Input<T> and Output<T>, including metadata with attributes

  • UI 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, and OutputAttribute to enhance the designer experience

  • Composite Activities: Building complex activities that compose other activities

  • Custom Outcomes: Defining multiple execution paths using the FlowNode attribute

  • Dependency 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:

  1. Explore Built-in Activities: Study Elsa's built-in activities for patterns and best practices

  2. Create Domain Activities: Build activity libraries specific to your business domain

  3. Share Activities: Package your activities as NuGet packages for reuse across projects

  4. Advanced Topics: Learn about custom UI components, field extensions, and Studio customization

Additional Resources

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