--- 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()`. ## Decide: Task or Event | Need | Use | |------|-----| | Perform an action and continue (log, notify, HTTP call, set value) | `TaskActivity` | | 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**: ```csharp public class LogTask : TaskActivity { private readonly IWorkflowExpressionEvaluator _expressionEvaluator; protected readonly IStringLocalizer S; public LogTask(IWorkflowExpressionEvaluator expressionEvaluator, IStringLocalizer 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 Text { get => GetProperty(() => new WorkflowExpression()); set => SetProperty(value); } public override IEnumerable GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext) => Outcomes(S["Done"]); public override async Task 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`: ```csharp 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 SignalName { get => GetProperty(() => new WorkflowExpression()); set => SetProperty(value); } public override async Task CanExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext) { var signalName = await _expressionEvaluator.EvaluateAsync(SignalName, workflowContext, null); return string.Equals(workflowContext.Input.GetValue("Signal"), signalName, StringComparison.OrdinalIgnoreCase); } public override IEnumerable 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 ```csharp public sealed class LogTaskDisplayDriver : ActivityDisplayDriver { 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(model.Text); } ``` `ActivityDisplayDriver` 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`: ```csharp services.AddActivity(); ``` ### 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`) | | `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]`): ```csharp 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 | ```csharp workflowContext.Output["Result"] = value; workflowContext.Properties["Counter"] = 1; var signal = workflowContext.Input.GetValue("Signal"); ``` ## Gotchas - Use `WorkflowExpression` for user-editable fields so authors can write Liquid/JS; evaluate with `IWorkflowExpressionEvaluator`. - Events must override `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. - 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 examples - `src/docs/reference/modules/Workflows/README.md` (repo) — official reference + built-in activity catalog - `src/docs/topics/workflows/README.md` (repo) — concepts - `AGENTS.md` (repo root) — build commands