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.
73
90%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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:
TaskActivity). Cannot start a workflow.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>().
| 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) |
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.
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.
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 |
In Startup.cs:
services.AddActivity<LogTask, LogTaskDisplayDriver>();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.
| 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 |
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) |
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);
}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");WorkflowExpression<string> for user-editable fields so authors can write Liquid/JS; evaluate with IWorkflowExpressionEvaluator.Name to a stable value (often a static EventName) — other code references it by name to resume.GetPossibleOutcomes returns Outcome objects (localized labels); ExecuteAsync returns ActivityExecutionResult via Outcomes("...") strings. Keep the names aligned.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 commandsb0e1e44
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.