Custom Activities
Release-backed guide to authoring custom Elsa activities in 3.8.0, including inputs, outputs, bookmarks, triggers, registration, Studio metadata, and testing.
This guide is based on the release/3.8.0 source code in elsa-core, elsa-studio, and elsa-extensions.
Use custom activities when you need to package domain logic, external system integration, or reusable workflow building blocks behind the same activity model that Elsa uses for its built-in activities.
Choose the Right Base Type
In release/3.8.0, the usual starting points are:
CodeActivity
The activity does its work and completes immediately.
CodeActivity<T>
Same as CodeActivity, but the activity also returns a typed result.
Activity
You need manual control over completion, child scheduling, or bookmarks.
Activity<T>
Same as Activity, but the activity also returns a typed result.
Trigger / Trigger<T>
The activity can start workflows and also behaves like a bookmark-driven waiting activity at runtime.
The important difference is completion behavior:
CodeActivityadds Elsa's auto-complete behavior, so you do not callCompleteActivityAsyncyourself.Activitydoes not auto-complete. If you neither complete the activity nor create bookmarks nor schedule child work, execution will stall.
A Minimal Immediate Activity
This is the simplest authoring path and maps directly to how many built-in activities are implemented.
using Elsa.Extensions;
using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Models;
[Activity("Acme", "Notifications", "Write a greeting to the console.")]
public class WriteGreeting : CodeActivity
{
[Input(Description = "The name to greet.")]
public Input<string> Name { get; set; } = new("world");
protected override void Execute(ActivityExecutionContext context)
{
var name = context.Get(Name);
Console.WriteLine($"Hello, {name}!");
}
}Notes:
Input<T>lets the property accept literals, variables, and installed expression syntaxes.context.Get(input)is the standard way to read evaluated input values.CodeActivitycompletes automatically afterExecutereturns.
Returning Data
If the activity produces a result, inherit from CodeActivity<T> or Activity<T>. Elsa exposes the Result output automatically.
Use Activity<T> instead when you need both a result and manual control over completion, outcomes, or bookmarks.
Inputs and Studio Metadata
Elsa Studio builds activity editors from the descriptor metadata that Elsa generates from your activity type.
The most important attribute is InputAttribute:
release/3.8.0 supports these InputAttribute capabilities that are especially useful for custom activities:
UIHintchooses the Studio editor component.Optionsprovides static choices for dropdown, checklist, and radio editors.DefaultSyntaxandSupportedSyntaxesshape the expression authoring experience.AutoEvaluate = falselets the activity evaluate the expression itself.CanContainSecrets = truemarks sensitive inputs such as tokens or passwords.UIHandlerandUIHandlersattach custom property UI handlers.
For the built-in hints and how Studio resolves them, see UI Hints.
To change how a custom activity appears in Studio's picker and designer, see Custom Activity Icons.
If the activity has dynamic outcomes or embedded child activities, see Activity Port Providers for the Studio-side port contract.
If several activities should share named external-system settings, see Connections for activities. That extension keeps the selected connection name in the workflow and resolves the typed settings at execution time.
Outputs
You can expose additional outputs by declaring Output<T> properties and setting them from the execution context.
Resolving Services
Custom activities are workflow model objects, not the usual place for constructor injection. In release/3.8.0, the normal pattern is to resolve services from ActivityExecutionContext.
That is the same pattern used by built-in activities such as Log, which resolves runtime services from the execution context.
Blocking Activities with Bookmarks
Use Activity or Activity<T> when the activity must pause and resume later. In Elsa, that is usually implemented by creating a bookmark.
This pattern is grounded in the same APIs used by built-in runtime activities such as Event, RunTask, DispatchWorkflow, and ExecuteWorkflow.
Use these rules when choosing bookmark behavior:
Keep the default
includeActivityInstanceId: truewhen the bookmark should resume one specific activity instance.Use
includeActivityInstanceId: falsewhen the stimulus identifies the logical wait point across instances, which is how runtime event-style activities are typically authored.Call
CompleteActivityAsyncfrom the resume callback when the activity should finish after resumption.
Trigger Activities
Trigger and Trigger<T> are for activities that can start workflows in addition to waiting inside a running workflow.
They have two responsibilities:
Provide trigger payloads for indexing through
GetTriggerPayload,GetTriggerPayloads, orGetTriggerPayloadsAsync.Create a bookmark in
ExecuteorExecuteAsyncso the activity can also wait and resume at runtime.
The built-in Event activity in Elsa.Workflows.Runtime follows this same split: trigger payload indexing on one side and bookmark-backed runtime waiting on the other.
Outcomes and Child Ports
There are two common ways to shape control flow from a custom activity.
Flowchart outcomes
Use FlowNodeAttribute when the activity should emit named flowchart outcomes.
Child activity ports
Use [Port] properties when the activity schedules other activities itself. The built-in If activity in Elsa.Workflows.Core is the reference pattern.
Registering Custom Activities
The activity type must be registered before Elsa can expose it through its activity registry and Studio descriptors.
In an application
This is the common host-level pattern and matches the sample server in elsa-core:
You can also register a single type:
In a module or feature
If you are authoring an Elsa module, use the module extensions:
For shell-feature-compatible service registration, release/3.8.0 also provides:
That path writes into ManagementOptions and is the service-collection equivalent of workflow management feature registration.
If your activity types are generated from external metadata or change at runtime, see Activity Type Providers.
Activity Hosts
If your use case maps naturally to method-based activities, Elsa also supports activity hosts.
The sample Penguin host in elsa-core is registered with:
Each public method becomes an activity, and method parameters become inputs except for special parameters such as ActivityExecutionContext and CancellationToken.
Use activity hosts when you want fast exposure of a service-like API as activities. Use normal activity classes when you need precise control over metadata, bookmarks, ports, or custom outputs.
Studio Customization Hooks
For most custom activities, ActivityAttribute, InputAttribute, and OutputAttribute are enough.
When they are not, release/3.8.0 provides two deeper hooks:
IActivityDescriptorModifierlets you reshape activity descriptors after registration.Property UI handlers let you provide dynamic options or editor metadata for specific inputs.
Use those when the Studio contract depends on runtime configuration or when static attributes are not expressive enough.
Related docs:
Testing Custom Activities
release/3.8.0 includes test helpers in Elsa.Testing.Shared.
For unit-style activity testing in isolation, use ActivityTestFixture:
ActivityTestFixture registers core workflow services, evaluates input properties, and builds an ActivityExecutionContext for the activity under test. For broader workflow-level coverage, Elsa.Testing.Shared also includes WorkflowTestFixture and TestApplicationBuilder.
Practical Guidance
Start with
CodeActivityunless you need bookmarks, child scheduling, or manual completion.Use
Input<T>for anything that should support variables or expressions.Use
CodeActivity<T>orActivity<T>when the activity returns a single primary result.Resolve services from
ActivityExecutionContext, not from custom constructors.Add Studio metadata deliberately. Good
Description,Category, andUIHintvalues matter for usability.Keep runtime behavior and Studio behavior aligned. If the activity requires a custom editor, document and ship that editor alongside the backend type.
Last updated