Add fluent workflow builder API for code-first workflows
Adds an optional fluent API over Elsa's existing workflow builder to reduce boilerplate and improve discoverability for code-first workflows. The API is a thin façade—all methods emit standard Elsa workflow structures.
Implementation
Core interfaces & builders:
-
IActivityBuilder- Interface for chaining activities -
ActivityBuilder- Chains activities into sequences automatically -
NestedActivityBuilder- Handles branch bodies (If/While/etc)
Extension methods:
-
WorkflowBuilderFluentExtensions-StartWith<T>(),WithVariable(),BuildAsync() -
ActivityBuilderExtensions-Then<T>(),SetVar(),Log(),Named() -
ControlFlowActivityBuilderExtensions-If(),While(),ForEach(),Switch(),Parallel() - Placeholder extensions for HTTP/scheduling activities with implementation guidance
Example
Before:
var workflow = new WorkflowDefinition
{
Activities = { new ReceiveOrder { Id = "receive" }, new If { Id = "check" } },
Connections = { new Connection("receive", "Done", "check"), ... }
};
After:
var workflow = await builder
.WithName("OrderApproval")
.WithVariable("Amount", 0m)
.StartWith<ReceiveOrder>(r => r.QueueName = "orders")
.If("Variables.Amount > 1000",
then: b => b.Log("Requires approval"),
@else: b => b.Log("Auto-approved"))
.BuildAsync();
Notes
- Zero breaking changes—extends existing
IWorkflowBuilderinterface only - Generates identical workflow structures to manual construction
- Extensible via standard extension methods for custom activities
- Full documentation in
FluentApiExamples.md
[!WARNING]
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
f.feedz.io
- Triggering command:
/opt/hostedtoolcache/CodeQL/2.23.3/x64/codeql/csharp/tools/linux64/Semmle.Autobuild.CSharp(dns block)If you need me to access, download, or install something from one of these locations, you can either:
- Configure Actions setup steps to set up my environment, which run before the firewall is enabled
- Add the appropriate URLs or hosts to the custom allowlist in this repository's Copilot coding agent settings (admins only)
Original prompt
This section details on the original issue you should resolve
<issue_title>Fluent workflow builder API for code-first workflows in Elsa 3</issue_title> <issue_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<TActivity>(Action<TActivity>? setup = null)
where TActivity : IActivity;
WorkflowDefinition Build();
}
public interface IActivityBuilder { IActivityBuilder Then<TActivity>(Action<TActivity>? 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<ReceiveOrder>(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...
- Fixes elsa-workflows/elsa-core#7064
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.