Skip to content

Workflow API

Core types for defining and executing workflows in Strategos.

Entry point for fluent workflow definitions. Creates a workflow builder for the specified state type.

MethodReturnsDescription
Create(string name)IWorkflowBuilder<TState>Creates a new workflow with the given name
Workflow<OrderState>.Create("process-order")
.StartWith<ValidateOrderStep>()
.Then<ProcessPaymentStep>()
.Finally<FulfillOrderStep>();

Interface for implementing workflow steps. Each step receives state, executes logic, and returns updated state.

MethodParametersReturnsDescription
ExecuteAsyncTState state, StepContext context, CancellationToken ctTask<StepResult<TState>>Executes the step logic
public class ValidateOrderStep : IWorkflowStep<OrderState>
{
public async Task<StepResult<OrderState>> ExecuteAsync(
OrderState state,
StepContext context,
CancellationToken ct)
{
var isValid = await ValidateAsync(state.Order, ct);
return state
.With(s => s.IsValid, isValid)
.AsResult();
}
}

Interface for workflow definition classes. Implemented by generated partial classes.

PropertyTypeDescription
DefinitionWorkflowDefinition<TState>The complete workflow definition
[Workflow("process-order")]
public static partial class ProcessOrderWorkflow : IWorkflowDefinition<OrderState>
{
public static WorkflowDefinition<OrderState> Definition =>
Workflow<OrderState>.Create("process-order")
.StartWith<ValidateOrderStep>()
.Finally<CompleteOrderStep>();
}

WorkflowActionReference in Strategos.Definitions is the immutable, language-neutral identity of the ontology action performed by one workflow step occurrence.

PropertyTypeDescription
DomainNamestringExact ontology domain name
ObjectTypeNamestringExact ontology object descriptor name
ActionNamestringExact ontology action name

The constructor rejects null, empty, and whitespace-only components and otherwise preserves each ordinal string as supplied. It carries no CLR type, so the identity can cross the workflow contract boundary without coupling a consumer to the producer’s runtime type system.

Use IStepConfiguration<TState>.Performs(WorkflowActionReference) on a class-based generic step occurrence:

Workflow<OrderState>.Create("process-order")
.StartWith<ValidateOrderStep>(step => step
.Performs(new WorkflowActionReference(
"Orders",
"Order",
"Validate")))
.Then<ProcessPaymentStep>(step => step
.Performs(new WorkflowActionReference(
"Orders",
"Order",
"CapturePayment"))
.WithRetry(3))
.Finally<FulfillOrderStep>(step => step
.Performs(new WorkflowActionReference(
"Orders",
"Order",
"Fulfill")));

Performs returns the same configuration builder, so it chains with retry, timeout, compensation, and confidence configuration. A step occurrence may declare it only once; a null reference throws ArgumentNullException and a second declaration throws InvalidOperationException.

Configured overloads are available wherever a class-based structural or handler step needs an occurrence identity, including top-level and loop fork joins plus approval rejection and timeout paths:

.Fork(
path => path.Then<ReserveInventoryStep>(),
path => path.Then<AuthorizePaymentStep>())
.Join<MergeOrderStep>(step => step.Performs(
new WorkflowActionReference("Orders", "Order", "Merge")))

The identity is occurrence-scoped. Reusing the same CLR step type in two fork, branch, loop, failure, or confidence-handler positions does not imply that both occurrences perform the same ontology action. The proof graph is keyed by the effective phase name, so occurrences that collapse to one phase identity cannot carry different references. Use distinct CLR step types, or distinct instance names where the builder exposes a combined name-and-configuration overload. There is no type-level/default action attribute.

StepDefinition.Action exposes the resulting nullable reference. Ordinary unbound workflows may leave it null. When an ontology action is bound to the workflow, however, every reachable named step occurrence must supply one closed reference so the generator can prove the workflow implementation against the action contract. Lambda/delegate steps do not expose Performs and therefore cannot serve as a proved leaf in a bound workflow.

For compile-time proof, use a direct new WorkflowActionReference(domainName, objectTypeName, actionName) with compile-time constant strings. The generator resolves that exact ordinal three-name tuple against the ontology action catalog. In a workflow that an ontology action binds, missing references, factories, dynamic expressions, blank names, multiple declarations, and zero or multiple catalog matches fail with AGWF040 rather than being accepted for runtime-only resolution. A workflow that no action binds is not proved, so its action references are carried but not checked. For imported workflow JSON, a malformed action object is rejected by the import front end as AGWF023; AGWF040 applies only after an action reference has been accepted into the workflow model.

Static binding proof reads one action catalog: the source declarations of the current compilation, the workflow JSON imported as AdditionalFiles, and the action contracts that referenced assemblies carry in their exported proof catalogs. It does not open arbitrary declarations inside referenced binaries and does not execute runtime IOntologySource contributions, and the proof is never repeated at runtime. A catalog carries contracts and not workflow models, so the workflow must be lowered in the compilation being built; a binding whose workflow is absent here is deferred to the one that lowers it.

The Contracts 0.11.0 workflow schema projects the value as the optional occurrence-level action object on every step kind:

{
"action": {
"domainName": "Orders",
"objectTypeName": "Order",
"actionName": "CapturePayment"
}
}

All three fields are required when action is present. Legacy and unconfigured workflow JSON omits the additive field byte-for-byte. See behavioral refinement and workflow bindings for the proof obligations and AGWF039AGWF043.

Use the typed Compensate overload to identify the ontology action implemented by an inverse step:

.Then<CapturePaymentStep>(step => step
.Performs(new WorkflowActionReference(
"Orders", "Order", "CapturePayment"))
.Compensate<RefundPaymentStep>(new WorkflowActionReference(
"Orders", "Order", "RefundPayment")))

The generator derives the required inverse contract from the forward action and proves that the authored inverse has the same subject, frame, and semantic authority, requires the forward effective guarantee, and re-enters the set of states described by the forward hard requirement. This proof does not establish restoration of the exact concrete pre-forward state or reversal of external effects; the frame is the same declared may-change boundary, not a snapshot. AGWF044 reports a disagreement. If the workflow or a bound action claims rollback, AGWF045 rejects a scope containing any rollback-reachable occurrence with a non-empty frame but no proved inverse. Typed compensation also rejects RequiredOnFailure = false; completed-prefix rollback is mandatory once the typed program claims rollback safety.

The existing no-argument .Compensate<T>() overload remains available for legacy runtime-only workflows. It carries no inverse action identity and cannot participate in static rollback proof. Contracts 0.12.0 projects the typed value as the optional compensation.inverseAction object using the same ActionReferenceV1 shape. Legacy JSON continues to omit the field. Each step occurrence accepts one compensation declaration; a second call to either overload throws InvalidOperationException rather than replacing the first executable/inverse pair.

At runtime, Strategos records an exact durable dispatch claim before each forward worker starts, consumes it into the completion journal, and derives the reverse plan from the completed prefix. A forged or stale execution identity cannot claim rollback. The failed occurrence is excluded. Nested failures unwind their innermost concrete scope; fork rollback waits for all lanes to become terminal. See mechanically derived compensation for the exact contract and durability rules.

Typed derived compensation currently requires PersistenceMode.SagaDocument. For PersistenceMode.EventSourced, the application owns ApplyEvent, so the generator cannot prove that a generated rollback-completed event applies its UpdatedState during both live handling and Marten replay. Such a typed program receives AGWF045; a no-op or pass-through ApplyEvent method is not accepted as proof. Legacy untyped compensation retains its existing event-sourced path.


Result type returned from step execution. Contains the updated state and optional routing information.

PropertyTypeDescription
StateTStateThe updated workflow state
BranchValueobject?Optional value for branch routing
IsCompleteboolWhether workflow should terminate
MethodDescription
state.AsResult()Creates result with updated state
state.AsResult(branchValue)Creates result with branch routing
StepResult<TState>.Complete(state)Creates terminal result
// Simple state update
return state.With(s => s.Status, "Validated").AsResult();
// With branch routing
return state.AsResult(state.OrderType); // Routes based on OrderType
// Terminal result
return StepResult<OrderState>.Complete(state);

Execution context passed to every step. Contains metadata about the current execution.

PropertyTypeDescription
CorrelationIdstringCorrelation ID for tracing; do not parse it as a durable identity
WorkflowIdGuidUnique identifier for this workflow instance
StepNamestringCurrent step name
TimestampDateTimeOffsetWhen the step execution started
CurrentPhasestringCurrent workflow phase name
RetryCountintNumber of retry attempts; defaults to zero
IsCompensationboolWhether this is an inverse execution; defaults to false
RollbackIdGuid?Stable identity shared by retries of one inverse execution; null during forward execution
public async Task<StepResult<OrderState>> ExecuteAsync(
OrderState state,
StepContext context,
CancellationToken ct)
{
_logger.LogInformation(
"Processing order {WorkflowId} at phase {Phase}",
context.WorkflowId,
context.CurrentPhase);
if (context is { IsCompensation: true, RollbackId: Guid rollbackId })
{
// At-least-once inverse delivery requires a durable idempotency key.
await _payments.RefundOnceAsync(rollbackId, state.PaymentId, ct);
}
// Step logic...
}

Attributes that control how state properties are merged between steps.

Marks a record as workflow state. Required for source generator to produce state reducers.

[WorkflowState]
public record OrderState
{
public Guid OrderId { get; init; }
public string Status { get; init; }
}

Merge lists by appending new items to existing items.

ConstraintValue
Valid OnCollection properties (List<T>, IList<T>, etc.)
BehaviorCombines source and target lists
[WorkflowState]
public record OrderState
{
[Append]
public List<string> AuditLog { get; init; } = new();
}

Merge Behavior:

// Before: AuditLog = ["Created", "Validated"]
// Update: AuditLog = ["Payment processed"]
// After: AuditLog = ["Created", "Validated", "Payment processed"]

Merge dictionaries. New values overwrite existing keys.

ConstraintValue
Valid OnDictionary properties (Dictionary<TKey, TValue>)
BehaviorCombines dictionaries, newer values win
[WorkflowState]
public record OrderState
{
[Merge]
public Dictionary<string, decimal> LinePrices { get; init; } = new();
}

Merge Behavior:

// Before: LinePrices = {"item1": 10.00, "item2": 20.00}
// Update: LinePrices = {"item2": 25.00, "item3": 30.00}
// After: LinePrices = {"item1": 10.00, "item2": 25.00, "item3": 30.00}

Methods available on the workflow builder for constructing workflow definitions.

MethodDescription
StartWith<TStep>()First step in workflow (required)
Then<TStep>()Sequential step
Finally<TStep>()Terminal step (recommended)
MethodDescription
Branch(selector, cases...)Route based on state value
BranchCase<TValue>(value, builder)Define branch case
Otherwise(builder)Default branch case
MethodDescription
Fork(paths...)Execute paths in parallel
Join<TStep>()Merge parallel results
Join<TStep>(configure)Merge parallel results and configure the join occurrence
MethodDescription
RepeatUntil(condition, name, builder)Loop until condition is true
MethodDescription
AwaitApproval<TStep>()Pause for human approval
Workflow<OrderState>.Create("process-order")
.StartWith<ValidateOrderStep>()
.Branch(s => s.OrderType,
BranchCase<OrderType>(OrderType.Standard, path => path
.Then<ProcessStandardStep>()),
BranchCase<OrderType>(OrderType.Express, path => path
.Then<ProcessExpressStep>()),
Otherwise(path => path
.Then<ProcessCustomStep>()))
.Fork(
path => path.Then<NotifyCustomerStep>(),
path => path.Then<UpdateInventoryStep>())
.Join<AggregateResultsStep>()
.AwaitApproval<ShipmentApprovalStep>()
.Finally<FulfillOrderStep>();