Writing Custom Tasks
Extend Aurora Workflows with your own task types
Overview
Any .NET class that implements IWorkflowTaskBase can be used as a task. In practice you always extend one of the provided abstract base classes so you only need to implement the three lifecycle methods.
| Base Class | Use when… |
|---|---|
WorkflowTaskBase | The standard base class for all tasks. Handles input/output, error handling, retry, cancellation, and expression evaluation via Resolve<T>(). Always extend this. |
ExpressionWorkflowTaskBase | Deprecated – will be removed in a future version. Adds a dedicated Expression property with ResolveExpression() helpers. For new tasks, extend WorkflowTaskBase and use Resolve<T>() directly on any string property. |
TransactionScopeTaskBase | Your task contains child tasks that run inside a private scope (loops, conditionals, etc.). |
Do not call
context.Get(), context.Set(), or context[key] inside DoAsync, DoSetupAsync, or DoTearDownAsync.Always use the inherited helper methods instead:
GetInput<T>(context)— read a typed value from the context, respecting scope and variable name settings.GetInput(context)— read an untyped value.SetOutput(context, value)— write a result to the context, respecting scope and variable name settings.Resolve<T>(rawValue, context)— evaluate a string property as a plain value or a C# expression and return the typed result.GetPreviousTaskOutput(context)— read the output of the immediately preceding task.
Minimal Example: WorkflowTaskBase
Extend WorkflowTaskBase and implement the three lifecycle methods. Use Resolve<T>(property, context) to evaluate any string property that should support C# expressions, GetInput<T>(context) to read variables from the data context, and SetOutput(context, value) to write results.
using System.Threading.Tasks;
using Aurora.Workflows.Contracts;
using Aurora.Workflows.Contracts.Attributes;
namespace MyCompany.Workflows.Tasks
{
[WorkflowTaskCategory("My Category")]
[WorkflowTaskOutputDefinition(typeof(string), IsDefault = true,
Description = "Returns a greeting string.")]
public sealed class GreetTask : WorkflowTaskBase
{
// String properties support plain values OR '=' prefixed C# expressions.
// Always resolve them with Resolve<T>() – never read them raw.
public string RecipientName { get; set; } = "World";
public GreetTask()
{
Name = "Greet Task";
}
protected override Task<bool> DoAsync(IWorkflowDataContext context)
{
// Resolve evaluates the property – plain text or '=expression'.
// Never use context.Get() / context.Set() here directly.
var name = Resolve<string>(RecipientName, context);
var greeting = $"Hello, {name}!";
// Write the result back via SetOutput – never via context[key] = ...
SetOutput(context, greeting);
return Task.FromResult(true);
}
protected override Task<bool> DoSetupAsync()
=> Task.FromResult(true);
protected override Task<bool> DoTearDownAsync()
=> Task.FromResult(true);
}
}
ExpressionWorkflowTaskBase ⚠️ Deprecated
WorkflowTaskBase directly and call Resolve<T>(myStringProperty, context) on any property that should support expressions. The section below is kept for reference only.Extending ExpressionWorkflowTaskBase adds a dedicated Expression string property and the ResolveExpression() / ResolveExpression<T>() helpers. All of this functionality is available in WorkflowTaskBase directly via Resolve<T>().
using System.ComponentModel;
using System.Threading.Tasks;
using Aurora.Workflows.Contracts;
using Aurora.Workflows.Contracts.Attributes;
namespace MyCompany.Workflows.Tasks
{
[WorkflowTaskCategory("My Category")]
[WorkflowTaskOutputDefinition(typeof(string), IsDefault = true)]
public sealed class FormatMessageTask : ExpressionWorkflowTaskBase
{
// Expose the base Expression property under a semantic name. Like every string task
// property it is expression-capable: plain text is a literal, "=..." is a C# expression.
[Category(TaskSpecificCategory)]
[DisplayName("Template")]
public string Template
{
get => base.Expression;
set => base.Expression = value;
}
// Hide the generic base property from IntelliSense and the designer.
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new string Expression
{
get => base.Expression;
set => base.Expression = value;
}
public bool ShouldSerializeExpression() => false;
public FormatMessageTask()
{
Name = "Format Message Task";
}
protected override Task<bool> DoAsync(IWorkflowDataContext context)
{
// ResolveExpression evaluates Template: literal text as-is, "=..." as C#
var result = ResolveExpression<string>();
SetOutput(context, result);
return Task.FromResult(true);
}
protected override Task<bool> DoSetupAsync() => Task.FromResult(true);
protected override Task<bool> DoTearDownAsync() => Task.FromResult(true);
}
}
Reading Input and Writing Output
Inside DoAsync always use the helper methods inherited from WorkflowTaskBase. Never access context directly with context.Get(), context.Set(), or the indexer — these bypass scope resolution and variable name configuration.
| Method | Description |
|---|---|
GetInput<T>(context) | Reads a typed value from the context, honouring CustomInputVariableName and all scope settings (UseGlobalScopeForInput, UseParentScopeForInput). |
GetInput(context) | Reads an untyped value, same scope rules as above. |
SetOutput(context, value) | Writes value to the context, honouring CustomOutputVariableName and all scope settings. Also sets the internal __LASTOUTPUT marker so the next task can pick it up automatically. |
GetPreviousTaskOutput(context) | Reads the __LASTOUTPUT value written by the immediately preceding sibling task. |
Resolve<T>(rawValue, context) | Evaluates a string property: plain text is returned as-is; a value prefixed with = is compiled and run as a C# expression by the Roslyn engine. Use this for every task property that should support expressions. |
Resolve(rawValue, context) | Same as above, untyped overload. |
Data | A ScopedDataAccessor bound to the context of the current execution. Offers the same semantics as the helpers above plus scope resolution helpers (GetInputScope(), GetOutputScope(), ...). Equivalent to context.For(this). See Data Context. |
All helpers delegate to ScopedDataAccessor, so GetInput<T>(context) and context.For(this).GetInput<T>() are guaranteed to behave identically.
Attributes
Attributes control how the designer and the runtime discover and categorise your task.
| Attribute | Target | Description |
|---|---|---|
[WorkflowTaskCategory(category, subCategory?)] | Class | Places the task in the designer palette under the given category. Use the constants in WorkflowTaskCategories or any string. |
[WorkflowTaskInputDefinition(type, ...)] | Class | Declares an expected input type and optional description. Can be applied multiple times. |
[WorkflowTaskOutputDefinition(type, ...)] | Class | Declares a produced output type. Mark the primary output with IsDefault = true. |
[Description("...")] | Class / Property | Shown in the designer as a tooltip or description. |
[DisplayName("...")] | Property | Overrides the property name shown in the designer property panel. |
[Category("...")] | Property | Groups the property under a named section in the property panel. |
[Browsable(false)] | Property | Hides the property from the designer. |
[NoExpression] | Property | Marks a property as not supporting Roslyn expression evaluation. |
[Experimental] | Class / Property | Marks a feature as experimental; shown with a warning badge in the designer. |
Scope Tasks (TransactionScopeTaskBase)
A scope task hosts child tasks in a dedicated scope. Extend TransactionScopeTaskBase and implement two methods:
public sealed class MyRepeatTask : TransactionScopeTaskBase
{
public int Count { get; set; } = 3;
public MyRepeatTask() { Name = "My Repeat Task"; }
// Called after the scope body executes; return true to continue.
protected override Task<bool> DoConclusionAsync(
IWorkflowDataContext context,
IWorkflowDataContext scopedContext)
=> Task.FromResult(true);
// The main scope logic. Call DoExecuteScopeBranchAsync to run children.
protected override async Task<IWorkflowDataContext> DoScopedAsync(
IWorkflowDataContext context)
{
IWorkflowDataContext result = null;
for (int i = 0; i < Count; i++)
{
result = await DoExecuteScopeBranchAsync(context);
}
return result;
}
}
Packaging and Loading Custom Tasks
Custom tasks are loaded into Aurora Workflows as standard .NET assemblies. There are two approaches:
1. Reference the assembly directly (design-time)
In the Desktop Application, add a reference to your assembly through the project settings. The designer will discover all task classes via reflection.
2. Bundle with a NuGet package (runtime)
Pack your task library as a NuGet package and place the .nupkg file in the .packages/ folder next to the CLI Runner. The workflow engine will load it automatically when the .awfc bundle references it.
3. Load at runtime with LoadAssemblyTask
Use the built-in LoadAssemblyTask inside a workflow to load a custom assembly from a file path at runtime before the tasks in that assembly are used.
Using the Logger
Inside any task method, use the inherited Logger property (type ILogger) to write structured log output:
protected override Task<bool> DoAsync(IWorkflowDataContext context)
{
Logger.LogInformation("Processing item {Item}", myItem);
// ...
return Task.FromResult(true);
}
The logger is automatically injected by the workflow engine when the task is initialised. You do not need to configure it yourself.
Cancellation
All task base classes expose a CancellationToken property that is signalled when the workflow is stopped. Pass it to any async operations that support cancellation:
await Task.Delay(5000, CancellationToken);
await httpClient.GetAsync(url, CancellationToken);
When a stop signal has already been received, ExecuteAsync skips the task silently instead of throwing. An OperationCanceledException thrown from DoAsync puts the task into the Cancelled state.
Status and Error Handling
WorkflowTaskBase tracks a single Status; the boolean properties IsRunning, HasCompleted, HasError and HasCancelled are derived from it.
| Status | When |
|---|---|
Idle | Never executed, reset, or skipped. |
Running | DoAsync is executing. |
Completed | DoAsync finished without an exception. Set before child tasks run so joining tasks can observe the parent. |
CompletedWithError | An exception was thrown but swallowed because ContinueOnError is enabled (or all retries failed with ContinueOnError). HasCompleted and HasError are both true; LastThrownException holds the exception and the context is flagged via __ERROROCCURED. |
Error | An exception escaped retry/continue-on-error handling. The exception is re-thrown to the caller and the workflow stops. HasCompleted is false. |
Cancelled | The task was cancelled or auto-cancelled because its parent returned false from DoAsync. |
Retry (RetryEnabled, MaxRetries, RetryInterval) is applied first; ContinueOnError only kicks in after the last retry has failed. Every new execution clears LastErrorMessage and LastThrownException.