name: orchardcore-workflow-activity description: Creates custom OrchardCore workflow activities — tasks and events — with their display drivers and registration. Use when the user needs a new workflow Task or Event, custom outcomes, activity editor UI, or to read/write workflow input, output, and properties.
OrchardCore Workflow Activity
This skill guides you through writing custom workflow activities following project conventions.
A workflow is a graph of activities connected by outcomes. Two activity kinds:
- Task — does work, then returns one or more outcomes (
TaskActivity). Cannot start a workflow. - Event — waits for something to happen, halting the workflow until resumed (
EventActivity). Can be a workflow start point.
Each activity pairs with a display driver that renders its thumbnail, design, and editor shapes, and is registered with services.AddActivity<TActivity, TDriver>().
Decide: Task or Event
| Need | Use |
|---|---|
| Perform an action and continue (log, notify, HTTP call, set value) | TaskActivity<T> |
| Wait for / react to an external trigger (signal, timer, HTTP request) | EventActivity |
| Start a workflow | Event (tasks can't start) |
Workflow
Step 1: Create the activity class
A task:
public class LogTask : TaskActivity<LogTask>
{
private readonly IWorkflowExpressionEvaluator _expressionEvaluator;
protected readonly IStringLocalizer S;
public LogTask(IWorkflowExpressionEvaluator expressionEvaluator, IStringLocalizer<LogTask> localizer)
{
_expressionEvaluator = expressionEvaluator;
S = localizer;
}
public override LocalizedString DisplayText => S["Log Task"];
public override LocalizedString Category => S["Primitives"];
// Persisted property — stored in the activity's Properties bag
public WorkflowExpression<string> Text
{
get => GetProperty(() => new WorkflowExpression<string>());
set => SetProperty(value);
}
public override IEnumerable<Outcome> GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
=> Outcomes(S["Done"]);
public override async Task<ActivityExecutionResult> ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
{
var text = await _expressionEvaluator.EvaluateAsync(Text, workflowContext, null);
// ... do work ...
return Outcomes("Done");
}
}
An event halts in Execute, gates on CanExecuteAsync, and resumes in Resume:
public class SignalEvent : EventActivity
{
public static string EventName => nameof(SignalEvent);
public override string Name => EventName;
public override LocalizedString DisplayText => S["Signal Event"];
public override LocalizedString Category => S["HTTP"];
public WorkflowExpression<string> SignalName
{
get => GetProperty(() => new WorkflowExpression<string>());
set => SetProperty(value);
}
public override async Task<bool> CanExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
{
var signalName = await _expressionEvaluator.EvaluateAsync(SignalName, workflowContext, null);
return string.Equals(workflowContext.Input.GetValue<string>("Signal"), signalName, StringComparison.OrdinalIgnoreCase);
}
public override IEnumerable<Outcome> GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
=> Outcomes(S["Done"]);
public override ActivityExecutionResult Resume(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
=> Outcomes("Done");
}
EventActivity.Execute returns Halt() by default — the workflow persists and waits.
Step 2: Create the display driver
public sealed class LogTaskDisplayDriver : ActivityDisplayDriver<LogTask, LogTaskViewModel>
{
protected override void EditActivity(LogTask activity, LogTaskViewModel model)
=> model.Text = activity.Text.Expression;
protected override void UpdateActivity(LogTaskViewModel model, LogTask activity)
=> activity.Text = new WorkflowExpression<string>(model.Text);
}
ActivityDisplayDriver<TActivity, TEditViewModel> wires thumbnail/design/edit shapes automatically. You only map between activity and view model.
Step 3: Create the shape templates
Three Razor shapes, named by activity:
| Shape | File | Renders |
|---|---|---|
| Thumbnail | LogTask_Fields_Thumbnail.cshtml |
activity picker entry |
| Design | LogTask_Fields_Design.cshtml |
node on the workflow canvas |
| Edit | LogTask_Fields_Edit.cshtml |
property editor form |
Step 4: Register
In Startup.cs:
services.AddActivity<LogTask, LogTaskDisplayDriver>();
Step 5: Test
Build, open Admin → Workflows, create a workflow, drop your activity, wire its outcome, run. Events: trigger the external condition and confirm the halted workflow resumes.
Quick Reference
Required / common members
| Member | Purpose |
|---|---|
Name |
technical id (defaults to type name via TaskActivity<T>) |
DisplayText |
localized label (S["..."]) |
Category |
grouping in the activity picker |
GetPossibleOutcomes(...) |
declares outcome ports |
ExecuteAsync(...) |
task work; return Outcomes(...) |
CanExecuteAsync(...) |
event gate (true → may run/resume) |
Resume(...) / ResumeAsync(...) |
event continuation after halt |
Outcome & result helpers (from Activity)
| Call | Meaning |
|---|---|
Outcomes("Done") |
continue down the Done port |
Outcomes("Yes", "No") |
multiple outcomes |
Halt() |
suspend (event waiting) |
Noop() |
empty result |
Outcomes(S["Done"]) |
declare a possible outcome (in GetPossibleOutcomes) |
Persisting state
GetProperty/SetProperty store into the activity's Properties JSON bag, keyed by member name ([CallerMemberName]):
public LogLevel LogLevel
{
get => GetProperty(() => LogLevel.Information); // default if unset
set => SetProperty(value);
}
Workflow-level data (WorkflowExecutionContext)
| Dict | Use |
|---|---|
Input |
values supplied by the initiator (read) |
Output |
values returned to the initiator (write) |
Properties |
shared state between activities |
workflowContext.Output["Result"] = value;
workflowContext.Properties["Counter"] = 1;
var signal = workflowContext.Input.GetValue<string>("Signal");
Gotchas
- Use
WorkflowExpression<string>for user-editable fields so authors can write Liquid/JS; evaluate withIWorkflowExpressionEvaluator. - Events must override
Nameto a stable value (often astatic EventName) — other code references it by name to resume. GetPossibleOutcomesreturnsOutcomeobjects (localized labels);ExecuteAsyncreturnsActivityExecutionResultviaOutcomes("...")strings. Keep the names aligned.- Long-running events persist the workflow to the DB; don't hold non-serializable state on the activity between halt and resume.
References
references/activities.md— base classes, state, outcomes, expression evaluation, full examplessrc/docs/reference/modules/Workflows/README.md(repo) — official reference + built-in activity catalogsrc/docs/topics/workflows/README.md(repo) — conceptsAGENTS.md(repo root) — build commands