elsa-workflows / elsa-workflows/elsa-core
Fluent workflow builder API for code-first workflows in Elsa 3
- Dominant language
- C#
- Stars
- 7.9k
- Forks
- 1.5k
- Avg merge
- 15h 22m
- Merged PRs (30d)
- 114
Description
Summary
Elsa 3 already supports code-first workflows by instantiating activity classes and constructing activity graphs directly in C#. This is powerful and explicit, but can be verbose and less discoverable for common patterns.
This feature request proposes an optional Fluent Workflow Builder API on top of the existing model. The fluent API would remain a thin façade over the existing WorkflowDefinition / activity graph and would not introduce a competing DSL. It aims to make code-first workflows more expressive, readable, and discoverable, while still mapping 1:1 to the underlying model.
⸻
Motivation
Current situation
A typical code-first workflow today might look like:
var workflow = new WorkflowDefinition
{
Id = "OrderApproval",
Version = 1,
Activities =
{
new ReceiveOrder
{
Id = "receive-order",
QueueName = "orders"
},
new ValidateOrder
{
Id = "validate-order"
},
new If
{
Id = "check-amount",
ConditionExpression = "Variables.Amount > 1000",
ThenOutcome = "HighAmount",
ElseOutcome = "NormalAmount"
},
},
Connections =
{
new Connection("receive-order", "Done", "validate-order"),
new Connection("validate-order", "Done", "check-amount"),
new Connection("check-amount", "HighAmount", "manager-approval"),
new Connection("check-amount", "NormalAmount", "auto-approve"),
}
};
This approach is explicit but requires:
• Manually managing IDs and outcomes
• Boilerplate for wiring connections
• Little IntelliSense guidance for control-flow structures
• No ergonomic shortcuts for common patterns
Why a fluent API helps
• Improves readability of complex workflows
• Suggests next steps through IntelliSense
• Standardizes branching/looping patterns
• Reduces boilerplate while remaining fully compatible with Elsa’s workflow graph
The key principle: the fluent API must be a thin façade that emits the same Elsa 3 workflow structure without introducing a separate DSL or runtime.
⸻
Proposed design (high level)
• A minimal set of core interfaces:
public interface IWorkflowBuilder
{
IWorkflowBuilder WithId(string id);
IWorkflowBuilder WithName(string name);
IWorkflowBuilder WithVersion(int version);
IWorkflowBuilder WithVariable(string name, object? defaultValue = null);
IActivityBuilder StartWith(Action? setup = null)
where TActivity : IActivity;
WorkflowDefinition Build();
}
public interface IActivityBuilder
{
IActivityBuilder Then(Action? setup = null)
where TActivity : IActivity;
}
• Entry point:
public static class WorkflowBuilder
{
public static IWorkflowBuilder Create(string name, int version = 1);
}
• All expressive functionality provided by extension methods:
• Control flow: If, Switch, Parallel, While, ForEach, Try
• Data helpers: SetVar, Log
• Bookmarks: Delay, WaitForSignal
• Integration: HttpGet, HttpPost, RunWorkflow
• Utility/misc: Named, Tag, etc.
• The fluent layer writes the same activities + outcomes + connections that Elsa 3 already expects.
⸻
Example workflow using the proposed fluent API
This sample shows what an “Order Approval” workflow could look like with a fluent-style builder.
var workflow = WorkflowBuilder
.Create("OrderApproval", version: 1)
.WithId("order-approval")
.WithVariable("OrderId")
.WithVariable("Order")
.WithVariable("ValidationResult")
.WithVariable("PaymentResult")
.WithVariable("PaymentSucceeded", false)
.WithVariable("Amount", 0m)
// 1. Receive order
.StartWith(x => x.QueueName = "orders")
.Named("Receive order")
.SetVar("OrderId", "input.OrderId")
.Log("Received order {{ Variables.OrderId }}")
// 2. Load order details
.HttpGet("https://api.myshop.local/orders/{{ Variables.OrderId }}", responseVar: "Order")
.SetVar("Amount", "Variables.Order.TotalAmount")
.Log("Loaded order (Amount = {{ Variables.Amount }})")
// 3. Validate order
.RunWorkflow("ValidateOrderWorkflow", new { Order = "{{ Variables.Order }}" }, outputVar: "ValidationResult")
.If("!Variables.ValidationResult.IsValid",
then: b => b
.Log("Order invalid.")
.RunWorkflow("NotifyCustomerWorkflow", new
{
Order = "{{ Variables.Order }}",
Reason = "{{ Variables.ValidationResult.Reason }}"
})
.Log("Workflow finished (invalid order).")
)
// 4. Manager approval or auto-approval depending on amount
.If("Variables.Amount > 1000",
then: high => high
.Log("Large order; requesting manager approval")
.RunWorkflow("CreateApprovalTaskWorkflow", new
{
Order = "{{ Variables.Order }}",
RequiredRole = "Manager"
})
.WaitForSignal("ManagerApproved:{{ Variables.OrderId }}")
.Log("Manager approved."),
@else: normal => normal
.Log("Order auto-approved.")
.RunWorkflow("AutoApproveOrderWorkflow", new { Order = "{{ Variables.Order }}" })
)
// 5. Payment with try/catch/finally
.Try(
@try: t => t
.HttpPost("https://payments.local/api/charge",
bodyExpression: "new { OrderId = Variables.OrderId, Amount = Variables.Amount }",
responseVar: "PaymentResult")
.SetVar("PaymentSucceeded", "Variables.PaymentResult.Success"),
@catch: c => c
.SetVar("PaymentSucceeded", false)
.Log("Payment failed: {{ LastError.Message }}")
.RunWorkflow("NotifyPaymentFailureWorkflow", new
{
Order = "{{ Variables.Order }}",
Error = "{{ LastError.Message }}"
}),
@finally: f => f
.Log("Payment attempt complete for order {{ Variables.OrderId }}.")
)
// 6. Send final approval status to customer
.If("Variables.PaymentSucceeded",
then: b => b
.RunWorkflow("NotifyCustomerOrderApprovedWorkflow", new { Order = "{{ Variables.Order }}" })
.Log("Order fully approved and customer notified."),
@else: b => b
.Log("Approval incomplete; payment failure."))
.Build();
This is only an illustration of what the API could look like.
It demonstrates:
• A natural, expressive workflow structure
• Reduction of boilerplate without hiding Elsa’s actual runtime concepts
• Easy integration for custom activities through straightforward extension methods
⸻
Important note
All examples above are conceptual.
Before introducing a fluent API, we should analyze:
• The existing workflow builder APIs in Elsa 3
• The current activity model and outcome mechanics
• Real-world usage patterns in the test projects
• How the fluent layer can be implemented cleanly on top of Elsa’s actual workflow structures
The fluent API must fit naturally into the existing architecture without introducing inconsistencies or parallel models.
Contributor guide
Assessment
This issue has not been assessed yet.