Plugins & Modules
Complete guide to extending Elsa Workflows with custom modules, features, and activities. Learn how to create reusable plugins and distribute them as NuGet packages.
Elsa Workflows provides a powerful and flexible extensibility system that allows you to create custom modules, features, and activities tailored to your specific needs. This guide will teach you how to extend Elsa with your own functionality and package it for reuse across projects.
Overview
The Elsa extensibility model is built around three core concepts:
Modules: Containers that group related features and provide a unified configuration interface
Features: Self-contained units of functionality that register services, activities, and other components
Activities: The building blocks of workflows that encapsulate specific actions or operations
This architecture enables you to:
Create domain-specific activities that encapsulate business logic
Package and distribute reusable extensions as NuGet packages
Maintain clean separation of concerns in large applications
Configure complex functionality through simple, fluent APIs
Table of Contents
Key Concepts
Modules & Features
Elsa uses a hierarchical configuration system where modules contain features, and features register the actual services and components.
IModule Interface
The IModule interface represents a container for features. The Elsa configuration system provides a default module implementation that you'll typically work with through extension methods.
FeatureBase Class
FeatureBase is the base class for all features in Elsa. A feature:
Encapsulates related functionality
Registers services with dependency injection
Can configure workflow options
Follows a two-phase initialization:
Configure()andApply()
Lifecycle Methods:
Configure(): Called during application startup to register services and configure options. This is where you:
Register activities using
AddActivitiesFrom<T>()Register workflows using
AddWorkflowsFrom<T>()Add custom services to the DI container
Configure workflow options
Apply(): Called after all features have been configured. Use this for:
Post-configuration tasks that depend on other features
Final validation
Complex initialization logic
UseXyz() Pattern
Elsa follows a convention where features are enabled using UseXyz() extension methods. This pattern:
Provides a fluent, discoverable API
Allows optional configuration via lambda expressions
Returns
IModulefor method chaining
Example:
builder.Services.AddElsa(elsa => elsa
.UseMyFeature()
.UseAnotherFeature(feature =>
{
// Configure the feature
})
);Activity Discovery & Registration
Elsa provides several methods for registering activities and workflows:
AddActivitiesFrom()
Scans the assembly containing type T and registers all classes marked with the [Activity] attribute:
Module.AddActivitiesFrom<MyFeature>();This method:
Discovers all activity classes in the assembly
Registers them with the activity registry
Makes them available in the workflow designer
AddWorkflowsFrom()
Similar to AddActivitiesFrom<T>(), but registers workflow definitions:
Module.AddWorkflowsFrom<MyFeature>();Creating a Custom Feature
Let's walk through creating a custom feature step by step.
Step 1: Define Your Feature Class
Create a class that inherits from FeatureBase:
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Microsoft.Extensions.DependencyInjection;
namespace MyWorkflows.Features;
public class MyFeature : FeatureBase
{
public MyFeature(IModule module) : base(module)
{
}
public override void Configure()
{
// Configuration goes here
}
public override void Apply()
{
// Post-configuration goes here (optional)
}
}Key Points:
The constructor must accept
IModuleand pass it to the base classOverride
Configure()to register services and componentsOverride
Apply()only if you need post-configuration logic
Step 2: Configure Services
Inside the Configure() method, register your activities and services:
public override void Configure()
{
// Register activities from this assembly
Module.AddActivitiesFrom<MyFeature>();
// Register custom services
Services.AddSingleton<IMyCustomService, MyCustomService>();
Services.AddScoped<IMyRepository, MyRepository>();
// Configure workflow options
Module.ConfigureWorkflowOptions(options =>
{
// Register UI hint handlers
options.RegisterUIHintHandler<MyCustomUIHintHandler>("MyCustomHint");
});
}Available Registration Methods:
Module.AddActivitiesFrom<T>(): Register all activities in the assemblyModule.AddWorkflowsFrom<T>(): Register all workflow definitions in the assemblyServices.Add...(): Access the service collection directly for custom registrationsModule.ConfigureWorkflowOptions(): Configure workflow-specific settings
Step 3: Create Extension Methods
Create a static extension class with UseXyz() methods following the Elsa convention:
using Elsa.Features.Services;
using MyWorkflows.Features;
namespace MyWorkflows.Extensions;
public static class ModuleExtensions
{
public static IModule UseMyFeature(
this IModule module,
Action<MyFeature>? configure = null)
{
module.Use(configure);
return module;
}
}Pattern with Options:
For more complex configuration, use an options class:
public static class ModuleExtensions
{
public static IModule UseMyFeature(
this IModule module,
Action<MyFeatureOptions>? configure = null)
{
return module.Use<MyFeature>(feature =>
{
if (configure != null)
{
var options = new MyFeatureOptions();
configure(options);
// Apply options to feature properties or services
if (options.EnableAdvancedFeatures)
{
feature.Services.AddSingleton<IAdvancedService, AdvancedService>();
}
}
});
}
}
public class MyFeatureOptions
{
public bool EnableAdvancedFeatures { get; set; } = false;
public string? ApiKey { get; set; }
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(30);
}Step 4: Register Your Feature
In your application's Program.cs or Startup.cs, use your extension method:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddElsa(elsa => elsa
.UseMyFeature()
// Or with configuration:
.UseMyFeature(options =>
{
options.EnableAdvancedFeatures = true;
options.ApiKey = builder.Configuration["MyFeature:ApiKey"];
})
);Creating Custom Activities
Custom activities are the primary way to extend workflow functionality. See examples/SampleActivity.cs for a complete example.
Basic Activity Structure
Activities inherit from CodeActivity or CodeActivity<T> (for activities with outputs):
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
[Activity("MyWorkflows", "Sample", "Description of what this activity does")]
public class SampleActivity : CodeActivity<string>
{
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// Activity logic here
// Set output if using CodeActivity<T>
context.Set(Result, "output value");
// Complete the activity
await context.CompleteActivityAsync();
}
}Base Classes:
CodeActivity: For activities without a return valueCodeActivity<T>: For activities that produce a single output of typeTActivity: For more complex activities with custom behavior
Defining Inputs and Outputs
Use the [Input] and [Output] attributes to define activity ports:
[Activity("MyWorkflows", "Data", "Processes a message with optional prefix")]
public class ProcessMessage : CodeActivity<string>
{
[Input(Description = "The message to process")]
public Input<string> Message { get; set; } = default!;
[Input(
Description = "Optional prefix to prepend",
DefaultValue = "INFO")]
public Input<string?> Prefix { get; set; } = default!;
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var message = context.Get(Message);
var prefix = context.Get(Prefix);
var result = $"{prefix}: {message}";
context.Set(Result, result);
await context.CompleteActivityAsync();
}
}Input/Output Features:
Description: Shown in the workflow designerDefaultValue: Default value if not specifiedUIHint: Custom UI control for the property editorCategory: Groups related properties in the designer
Activity Attributes
The [Activity] attribute configures how the activity appears in the designer:
[Activity(
Namespace = "MyCompany.MyProduct", // Logical grouping
Category = "Integration", // Designer category
Description = "Detailed description", // Shown in tooltips
DisplayName = "My Activity" // Display name (optional)
)]
public class MyActivity : CodeActivity
{
// ...
}Attribute Parameters:
Namespace: Groups activities logically (e.g., "MyCompany.Integration")
Category: Organizes activities in the designer toolbox
Description: Provides help text for workflow designers
DisplayName: Overrides the class name in the designer
Registering Activities
Activities are registered via features:
public override void Configure()
{
// Registers all activities in the assembly containing MyFeature
Module.AddActivitiesFrom<MyFeature>();
}This scans for all types marked with [Activity] and registers them with the activity registry. They become immediately available in:
The workflow designer
Programmatic workflow definitions
The workflow execution engine
Packaging & Distribution
To share your custom modules, package them as NuGet packages:
1. Create a Class Library Project
dotnet new classlib -n MyWorkflows.Extensions
cd MyWorkflows.Extensions
dotnet add package Elsa
dotnet add package Elsa.Workflows.Core2. Organize Your Code
MyWorkflows.Extensions/
├── Activities/
│ ├── SampleActivity.cs
│ └── AnotherActivity.cs
├── Features/
│ └── MyFeature.cs
├── Extensions/
│ └── ModuleExtensions.cs
└── MyWorkflows.Extensions.csproj3. Configure the .csproj File
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PackageId>MyCompany.MyWorkflows.Extensions</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Description>Custom Elsa Workflows extensions</Description>
<PackageTags>elsa;workflows;extensions</PackageTags>
<RepositoryUrl>https://github.com/yourorg/yourrepo</RepositoryUrl>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Elsa" Version="3.0.*" />
<PackageReference Include="Elsa.Workflows.Core" Version="3.0.*" />
</ItemGroup>
</Project>4. Build and Publish
dotnet pack -c Release
dotnet nuget push bin/Release/MyCompany.MyWorkflows.Extensions.1.0.0.nupkg \
--api-key YOUR_API_KEY \
--source https://api.nuget.org/v3/index.json5. Consume the Package
Users can then install and use your package:
dotnet add package MyCompany.MyWorkflows.Extensionsbuilder.Services.AddElsa(elsa => elsa
.UseMyFeature()
);Advanced Topics
Custom UI Hint Handlers
UI hint handlers control how activity properties are edited in the workflow designer:
public class MyCustomUIHintHandler : IUIHintHandler
{
public string UIHint => "MyCustomHint";
public object GetDefaultValue()
{
return new MyCustomData();
}
}Register in your feature:
Module.ConfigureWorkflowOptions(options =>
{
options.RegisterUIHintHandler<MyCustomUIHintHandler>("MyCustomHint");
});Custom Serializers
For complex data types, implement custom serializers:
public class MyTypeSerializer : ISerializer
{
public object Deserialize(string data)
{
// Deserialization logic
}
public string Serialize(object obj)
{
// Serialization logic
}
}Register in your feature:
Services.AddSingleton<ISerializer, MyTypeSerializer>();Activity Execution Context
The ActivityExecutionContext provides access to:
Workflow Instance: Current workflow state and variables
Input/Output: Get and set activity inputs and outputs
Journal: Log custom data for debugging
Cancellation: Handle workflow cancellation
Services: Access dependency injection container
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// Access workflow variables
var workflowVar = context.GetWorkflowVariable<string>("MyVar");
// Access services
var myService = context.GetRequiredService<IMyService>();
// Log to journal
context.JournalData.Add("CustomKey", "CustomValue");
// Check cancellation
if (context.CancellationToken.IsCancellationRequested)
return;
// ... activity logic
}Complete Examples
For complete, working examples, see:
SampleActivity.cs - A full custom activity with inputs and outputs
MyFeature.cs - A complete feature implementation
ModuleExtensions.cs - Extension methods following Elsa conventions
Best Practices
Follow Naming Conventions
Use
UseXyz()for feature extension methodsName features as
XyzFeatureUse clear, descriptive activity names
Provide Good Metadata
Use descriptive
[Activity]attributesAdd meaningful descriptions to inputs/outputs
Include usage examples in XML comments
Handle Errors Gracefully
Validate inputs in activities
Provide helpful error messages
Consider retry logic for transient failures
Test Thoroughly
Unit test activities independently
Integration test features
Test with the workflow designer
Document Your Extensions
Include XML documentation comments
Provide usage examples
Document configuration options
Further Reading
Custom Activities Guide - Detailed activity creation guide
Elsa Core Repository - Official source code and examples
Feature Documentation - Built-in features reference
Support
For questions and support:
Last updated