Workflow API
Workflow API
Section titled “Workflow API”Core types for defining and executing workflows in Strategos.
Workflow<TState>
Section titled “Workflow<TState>”Entry point for fluent workflow definitions. Creates a workflow builder for the specified state type.
Methods
Section titled “Methods”| Method | Returns | Description |
|---|---|---|
Create(string name) | IWorkflowBuilder<TState> | Creates a new workflow with the given name |
Example
Section titled “Example”Workflow<OrderState>.Create("process-order") .StartWith<ValidateOrderStep>() .Then<ProcessPaymentStep>() .Finally<FulfillOrderStep>();IWorkflowStep<TState>
Section titled “IWorkflowStep<TState>”Interface for implementing workflow steps. Each step receives state, executes logic, and returns updated state.
Methods
Section titled “Methods”| Method | Parameters | Returns | Description |
|---|---|---|---|
ExecuteAsync | TState state, StepContext context, CancellationToken ct | Task<StepResult<TState>> | Executes the step logic |
Example
Section titled “Example”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(); }}IWorkflowDefinition<TState>
Section titled “IWorkflowDefinition<TState>”Interface for workflow definition classes. Implemented by generated partial classes.
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
Definition | WorkflowDefinition<TState> | The complete workflow definition |
Example
Section titled “Example”[Workflow("process-order")]public static partial class ProcessOrderWorkflow : IWorkflowDefinition<OrderState>{ public static WorkflowDefinition<OrderState> Definition => Workflow<OrderState>.Create("process-order") .StartWith<ValidateOrderStep>() .Finally<CompleteOrderStep>();}Workflow action identity
Section titled “Workflow action identity”WorkflowActionReference in Strategos.Definitions is the immutable,
language-neutral identity of the ontology action performed by one workflow step
occurrence.
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
DomainName | string | Exact ontology domain name |
ObjectTypeName | string | Exact ontology object descriptor name |
ActionName | string | Exact 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.
Performs
Section titled “Performs”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 AGWF039–AGWF043.
Typed compensation
Section titled “Typed compensation”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.
StepResult<TState>
Section titled “StepResult<TState>”Result type returned from step execution. Contains the updated state and optional routing information.
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
State | TState | The updated workflow state |
BranchValue | object? | Optional value for branch routing |
IsComplete | bool | Whether workflow should terminate |
Creation Methods
Section titled “Creation Methods”| Method | Description |
|---|---|
state.AsResult() | Creates result with updated state |
state.AsResult(branchValue) | Creates result with branch routing |
StepResult<TState>.Complete(state) | Creates terminal result |
Example
Section titled “Example”// Simple state updatereturn state.With(s => s.Status, "Validated").AsResult();
// With branch routingreturn state.AsResult(state.OrderType); // Routes based on OrderType
// Terminal resultreturn StepResult<OrderState>.Complete(state);StepContext
Section titled “StepContext”Execution context passed to every step. Contains metadata about the current execution.
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
CorrelationId | string | Correlation ID for tracing; do not parse it as a durable identity |
WorkflowId | Guid | Unique identifier for this workflow instance |
StepName | string | Current step name |
Timestamp | DateTimeOffset | When the step execution started |
CurrentPhase | string | Current workflow phase name |
RetryCount | int | Number of retry attempts; defaults to zero |
IsCompensation | bool | Whether this is an inverse execution; defaults to false |
RollbackId | Guid? | Stable identity shared by retries of one inverse execution; null during forward execution |
Example
Section titled “Example”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...}State Attributes
Section titled “State Attributes”Attributes that control how state properties are merged between steps.
[WorkflowState]
Section titled “[WorkflowState]”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; }}[Append]
Section titled “[Append]”Merge lists by appending new items to existing items.
| Constraint | Value |
|---|---|
| Valid On | Collection properties (List<T>, IList<T>, etc.) |
| Behavior | Combines 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]
Section titled “[Merge]”Merge dictionaries. New values overwrite existing keys.
| Constraint | Value |
|---|---|
| Valid On | Dictionary properties (Dictionary<TKey, TValue>) |
| Behavior | Combines 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}Fluent Builder Methods
Section titled “Fluent Builder Methods”Methods available on the workflow builder for constructing workflow definitions.
Flow Control
Section titled “Flow Control”| Method | Description |
|---|---|
StartWith<TStep>() | First step in workflow (required) |
Then<TStep>() | Sequential step |
Finally<TStep>() | Terminal step (recommended) |
Branching
Section titled “Branching”| Method | Description |
|---|---|
Branch(selector, cases...) | Route based on state value |
BranchCase<TValue>(value, builder) | Define branch case |
Otherwise(builder) | Default branch case |
Parallel Execution
Section titled “Parallel Execution”| Method | Description |
|---|---|
Fork(paths...) | Execute paths in parallel |
Join<TStep>() | Merge parallel results |
Join<TStep>(configure) | Merge parallel results and configure the join occurrence |
| Method | Description |
|---|---|
RepeatUntil(condition, name, builder) | Loop until condition is true |
Human-in-the-Loop
Section titled “Human-in-the-Loop”| Method | Description |
|---|---|
AwaitApproval<TStep>() | Pause for human approval |
Example
Section titled “Example”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>();