Executive Summary
For .NET architects, workflow automation is no longer a luxury but a necessity. Business agility, auditability, and process transparency demand solutions that can flex with organizational needs. This article is a hands-on guide for building custom workflow engines in .NET using Business Process Model and Notation (BPMN). It dissects the theory, explores the latest open-source options, and leads you through practical implementation. You will leave with actionable insights and sample code to confidently design, build, and maintain robust workflow-driven applications that align with business goals.
1 The Strategic Imperative for Custom Workflows
Organizations are under constant pressure to streamline operations and respond quickly to change. Workflow automation promises faster time-to-market, consistency, and greater visibility. But choosing how to automate is as critical as the decision to automate itself.
1.1 Beyond Off-the-Shelf: When to Build vs. Buy
Workflow automation solutions span the spectrum from SaaS platforms (like Power Automate, Zapier, and Camunda Cloud) to low-code engines and developer-centric libraries. The decision to build or buy hinges on several factors:
- Complexity and Uniqueness: Off-the-shelf platforms excel for standard processes. But if your business processes are highly specialized, involve deep system integrations, or require granular control, custom workflows become necessary.
- Integration Requirements: Custom solutions shine when workflows must orchestrate internal systems, legacy platforms, or microservices that commercial platforms can’t handle natively.
- Ownership and Extensibility: Building in-house offers full control, the ability to embed business logic, and a platform for future innovation.
- Cost and Vendor Lock-In: While initial development is higher, custom solutions can reduce long-term licensing and support costs.
In practice, most organizations use a mix of both approaches, but high-value, core processes often justify the investment in custom workflows.
1.2 The Architect’s Role in Business Process Automation
As a .NET architect, your mandate is to align technical choices with business objectives. When it comes to process automation, you:
- Translate business requirements into executable workflows.
- Decide on the orchestration pattern: centralized engine, microservices-based, or hybrid.
- Evaluate tools for extensibility, testability, performance, and maintainability.
- Define the contract between business process models and code.
- Ensure workflows are observable, auditable, and secure.
You are both a translator and a builder—bridging the gap between BPMN diagrams drawn on whiteboards and resilient, scalable code running in production.
1.3 Introducing BPMN: More Than Just a Diagramming Standard
BPMN (Business Process Model and Notation) is a globally accepted standard for modeling business processes. But it is more than just a drawing tool for analysts; it is a shared language for developers, business stakeholders, and automated engines.
1.3.1 The Three “Flavors” of BPMN: Descriptive, Analytic, and Executable
- Descriptive BPMN: Focuses on high-level understanding. Used for process mapping and documentation. No strict semantics—ideal for initial discovery.
- Analytic BPMN: Adds precision for process improvement, risk analysis, and simulation. Still not meant for direct execution.
- Executable BPMN: The most formal, with well-defined semantics for automation. Can be parsed and run by workflow engines.
As a developer, you primarily interact with executable BPMN, either through code or by interpreting BPMN XML files.
1.3.2 Core BPMN Concepts for Developers: Events, Tasks, Gateways, and Pools/Lanes
- Events: Triggers or results within a process. Start events, end events, timers, messages.
- Tasks: Atomic units of work (e.g., service calls, user tasks).
- Gateways: Control flow (e.g., decisions, splits, joins).
- Pools/Lanes: Represent participants or departments.
Understanding how these map to engine primitives is key. For example, a BPMN “User Task” might correspond to a human workflow activity in code, while a “Service Task” could invoke an API.
2 The .NET Open-Source Workflow Engine Landscape (as of 2025)
The .NET ecosystem has matured significantly. While Microsoft’s Windows Workflow Foundation (WF) was the standard for a decade, modern alternatives are faster, more extensible, and better aligned with cloud-native architectures.
2.1 The Evolution from Windows Workflow Foundation (WF) to Modern Engines
WF was groundbreaking in the late 2000s but struggled with cloud readiness, extensibility, and BPMN support. Its XML-based workflows were difficult to version and test. Today, most architects look to new open-source engines that offer:
- Native .NET Core / .NET 6+ support
- Better support for BPMN, including import/export and runtime execution
- Extensible activity libraries for microservices, messaging, and long-running transactions
- Improved observability, monitoring, and state persistence
Let’s examine the leading engines.
2.2 A Comparative Analysis of Leading Libraries
2.2.1 Elsa Workflows: The .NET-Native, Highly Extensible Choice
Elsa Workflows is an open-source .NET workflow engine with native support for workflow modeling, execution, and persistence. As of 2025, Elsa v4 (latest major release) is highly extensible, supports BPMN 2.0 import/export, and features a modern designer UI.
Key Features:
- First-class .NET 8+ support
- Code-first and designer-first workflows
- BPMN 2.0 XML import/export
- Rich activity library (HTTP, email, timers, queues)
- Pluggable storage (EF Core, MongoDB, PostgreSQL, etc.)
- Integration with ASP.NET Core, Blazor, and minimal APIs
- Real-time workflow tracking and dashboard
Example: Implementing a Simple BPMN Workflow in Elsa
Suppose you have a BPMN process for leave request approval: an employee submits a request, HR reviews, and a manager approves or rejects.
Here’s a code-centric approach using Elsa:
public class LeaveRequestWorkflow : IWorkflow
{
public void Build(IWorkflowBuilder builder)
{
builder
.StartWith<ReceiveHttpRequest>(x => x.WithPath("/submit-leave").WithMethod(HttpMethods.Post))
.Then<RunJavaScript>(x => x.WithScript("console.log('Leave request submitted');"))
.Then<UserTask>(x => x.WithPrompt("HR Review"))
.Then<Switch>(x => x.WithExpression(context => context.GetVariable("isApproved")))
.When(true)
.Then<UserTask>(x => x.WithPrompt("Manager Approval"))
.Then<SendEmail>(x => x.WithSubject("Leave Approved"))
.When(false)
.Then<SendEmail>(x => x.WithSubject("Leave Rejected"))
.Then<Finish>();
}
}
Elsa supports both code-first and designer-first paradigms. You can import BPMN XML, extend activities, and integrate with modern application stacks.
2.2.2 Workflow Core: The Lightweight, Embeddable Engine
Workflow Core is a .NET Standard library aimed at lightweight workflow orchestration. It is particularly suited for embedding in microservices and headless applications where minimal dependencies are critical.
Key Features:
- Simple, fluent API for defining workflows in C#
- Lightweight and performant
- Supports persistence with various stores (SQL Server, MongoDB, Redis)
- Parallelism, branching, event handling
- Human task support via custom steps
Example: Parallel Approval Workflow in Workflow Core
Here’s how you might model a process requiring parallel approvals:
public class ParallelApprovalWorkflow : IWorkflow<MyData>
{
public void Build(IWorkflowBuilder<MyData> builder)
{
builder
.StartWith<InitialStep>()
.Parallel()
.Do(then => then.StartWith<ManagerApprovalStep>())
.Do(then => then.StartWith<HrApprovalStep>())
.Join()
.Then<SendResultEmailStep>();
}
}
Workflow Core is optimal when you need control, portability, and minimal runtime overhead.
2.2.3 Camunda (via .NET Clients): The Battle-Tested BPM Platform for Polyglot Environments
Camunda is a well-known Java-based BPM engine with a mature BPMN 2.0 implementation. For .NET, Camunda offers REST and gRPC APIs, and several community-supported .NET clients. This allows .NET applications to offload process execution while integrating with Camunda’s enterprise-grade tooling.
Key Features:
- Full BPMN 2.0 execution engine
- BPMN modeling tools (Camunda Modeler)
- REST/gRPC APIs for process interaction
- Advanced features (DMN rules, process analytics, tasklists)
- High scalability and clustering support
Example: Starting a BPMN Process from .NET Using Camunda REST
var client = new HttpClient();
var processInstance = await client.PostAsJsonAsync(
"https://your-camunda-server/engine-rest/process-definition/key/leaveRequest/start",
new { variables = new { employeeId = new { value = "12345", type = "String" } } }
);
Camunda is ideal for organizations standardizing on BPMN across heterogeneous stacks or requiring advanced workflow analytics.
2.2.4 Other Noteworthy Options and Their Niches
- NetBpm: Legacy open-source BPM engine for .NET, limited activity and BPMN 2.0 support.
- WorkflowEngine.NET: Commercial library with open-source core; offers UI tools, script integration, and strong support for designer-first workflows.
- Durable Functions (Azure): For orchestrating long-running workflows serverlessly in the Azure ecosystem; not BPMN-based but relevant for certain automation needs.
2.3 Architectural Decision Criteria: Choosing the Right Engine for Your Project
The ideal workflow engine depends on:
- BPMN Fidelity: Do you need full BPMN 2.0 support, or is a simplified workflow model sufficient?
- Hosting Model: Self-hosted, SaaS, hybrid?
- Integration Surface: REST APIs, messaging, microservices, external tasks?
- Extensibility: Ability to define custom activities, integrate with business logic.
- Observability: Out-of-the-box monitoring, persistence, audit trails.
- Performance and Scalability: High-throughput scenarios, clustering, multi-tenancy.
- Developer Experience: Code-first, designer-first, documentation, and community support.
For most .NET-centric projects seeking BPMN execution inside .NET, Elsa v4 is a strong contender. For large enterprises with mixed technology stacks, Camunda’s mature BPMN engine may be preferred. Workflow Core is a great fit for lightweight, headless orchestration.
3 Deep Dive: Building a Workflow with Elsa 3
3.1 Setting the Stage: A Real-World Scenario
Let’s ground our exploration in a scenario common to many organizations: Document Approval. In this process, an employee submits a document, which is then reviewed by an approver (typically a manager). The process may include:
- Initial submission and validation.
- Automatic checks (e.g., for compliance).
- Human review and approval (or rejection).
- Notifications at each step.
- Support for escalation if not reviewed in time.
This workflow is ideal for demonstrating how BPMN concepts and Elsa’s programming model combine to handle human and automated activities, state transitions, persistence, and extensibility.
Why does this matter? Because nearly every enterprise application deals with approval flows—contracts, expenses, leave requests, or publishing content. The value comes from customizing these processes for business needs, ensuring transparency, and being able to evolve them as the organization changes.
3.2 Getting Started with Elsa 3 in a .NET 8 Application
Elsa 3 is a modern, modular workflow engine purpose-built for .NET 6 and later. With strong BPMN 2.0 alignment, flexible persistence, and a plugin system, it fits naturally into contemporary .NET stacks.
Step 1: Create a New ASP.NET Core Project
Open a terminal and run:
dotnet new webapi -n DocumentApprovalDemo
cd DocumentApprovalDemo
Step 2: Add Elsa 3 Packages
Elsa is modular. For basic workflow authoring and execution, install:
dotnet add package Elsa.Workflows.Runtime
dotnet add package Elsa.Persistence.EntityFramework.SqlServer
For advanced scenarios, Elsa offers packages for Quartz (timers), MongoDB, dashboard UI, and more.
Step 3: Configure Elsa in Program.cs
Elsa uses .NET dependency injection and configures itself as middleware. Here’s how to wire it up:
using Elsa.Persistence.EntityFramework.Core.Extensions;
using Elsa.Persistence.EntityFramework.SqlServer;
using Elsa.Workflows.Runtime;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add Elsa services.
builder.Services.AddElsa(elsa => elsa
.UseEntityFrameworkPersistence(ef =>
ef.UseSqlServer(builder.Configuration.GetConnectionString("Elsa")))
.AddWorkflowsFrom<Program>() // Registers all IWorkflow implementations in assembly
);
// Configure database for Elsa persistence
builder.Services.AddDbContext<ElsaContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Elsa")));
// Add controllers.
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
This sets up Elsa to use SQL Server for workflow storage, but you can swap in other providers.
Step 4: Add a Database Connection String
In appsettings.json:
"ConnectionStrings": {
"Elsa": "Server=localhost;Database=ElsaDemoDb;User Id=sa;Password=your_password;"
}
Run migrations to initialize Elsa’s schema:
dotnet ef migrations add InitialCreate --context ElsaContext
dotnet ef database update --context ElsaContext
With the infrastructure ready, you can now model and run workflows.
3.3 Defining Workflows Programmatically with the C# Fluent API
Elsa’s C# Fluent API allows for full control and code-first workflow definitions. For our Document Approval scenario, we’ll create a workflow that:
- Receives a document submission via API.
- Runs compliance checks.
- Assigns the document for human approval.
- Handles approval or rejection paths.
- Sends notification emails.
Here’s how that looks:
using Elsa.Workflows;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Attributes;
public class DocumentApprovalWorkflow : WorkflowBase
{
protected override void Build(IWorkflowBuilder builder)
{
builder
.StartWith<ReceiveHttpRequest>(setup => setup
.WithPath("/api/documents/submit")
.WithMethod(HttpMethods.Post))
.Then<RunJavaScript>(setup => setup
.WithScript("return input.document.content.includes('confidential');")
.WithOutputVariable("requiresComplianceCheck"))
.Then<IfElse>(ifElse =>
{
ifElse.Condition = context => context.GetVariable<bool>("requiresComplianceCheck");
ifElse.WhenTrue = trueBranch => trueBranch
.Then<RunComplianceCheckActivity>();
ifElse.WhenFalse = falseBranch => falseBranch
.Then<SetVariable>(setup => setup.WithName("compliancePassed").WithValue(true));
})
.Then<IfElse>(ifElse =>
{
ifElse.Condition = context => context.GetVariable<bool>("compliancePassed");
ifElse.WhenTrue = trueBranch => trueBranch
.Then<AssignApprovalTaskActivity>();
ifElse.WhenFalse = falseBranch => falseBranch
.Then<SendEmail>(setup => setup
.WithTo(context => context.GetVariable<string>("submitterEmail"))
.WithSubject("Document Failed Compliance")
.WithBody("Your document failed compliance checks."));
})
.Then<IfElse>(ifElse =>
{
ifElse.Condition = context => context.GetVariable<bool>("isApproved");
ifElse.WhenTrue = approvedBranch => approvedBranch
.Then<SendEmail>(setup => setup
.WithTo(context => context.GetVariable<string>("submitterEmail"))
.WithSubject("Document Approved")
.WithBody("Your document has been approved."));
ifElse.WhenFalse = rejectedBranch => rejectedBranch
.Then<SendEmail>(setup => setup
.WithTo(context => context.GetVariable<string>("submitterEmail"))
.WithSubject("Document Rejected")
.WithBody("Your document has been rejected."));
});
}
}
A few things to note:
- Activities can reference variables and use expressions, enabling dynamic flow.
- You can introduce custom activities (like
RunComplianceCheckActivityorAssignApprovalTaskActivity) for organization-specific logic. - Elsa tracks workflow state, variables, and execution path automatically.
This approach gives you precise, testable, version-controlled workflows.
3.4 Creating Custom Activities: The Building Blocks of Your Business Logic
Elsa includes many built-in activities (HTTP, email, delays, switches). However, most real-world scenarios require custom activities to encapsulate proprietary logic, integrations, or human tasks.
Defining a Custom Activity
Suppose you want to perform a compliance check via an internal API. Here’s a custom activity:
using Elsa.Workflows;
using Elsa.Workflows.Models;
public class RunComplianceCheckActivity : Activity
{
[ActivityInput] public string DocumentId { get; set; }
[ActivityOutput] public bool Passed { get; set; }
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var complianceService = context.GetRequiredService<IComplianceService>();
Passed = await complianceService.CheckDocumentAsync(DocumentId);
context.SetVariable("compliancePassed", Passed);
}
}
Register this activity with Elsa’s DI container, and it becomes available in all workflows.
Encapsulating Human Tasks
Human-in-the-loop steps, such as manager approvals, can be modeled as custom activities that create pending tasks in a database, notify users, and resume the workflow upon completion. Elsa 3 supports blocking activities and signal-based resumption, which are key for human workflows.
Here’s a sketch:
public class AssignApprovalTaskActivity : Activity
{
[ActivityInput] public string DocumentId { get; set; }
[ActivityInput] public string ApproverEmail { get; set; }
protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// Save approval task to DB, notify approver.
// Block workflow until signal/event.
context.WorkflowExecutionContext.Block("ApprovalTask", DocumentId);
return ValueTask.CompletedTask;
}
}
Later, your API can signal the workflow to continue:
await workflowRuntime.ResumeWorkflowAsync(workflowInstanceId, "ApprovalTask", documentId);
This enables robust support for long-running, human-centric processes.
3.5 Managing Workflow State: Persistence and Long-Running Workflows
Modern workflow engines must support long-running, persistent workflows—think days or weeks for approvals, escalations, or SLAs. Elsa supports this natively.
What Does Workflow State Include?
- Current activity/step
- Variable values
- Execution history
- Bookmarks and blocking activities
- Correlation data (for resuming workflows on specific events)
Elsa serializes this state and stores it in your chosen persistence layer.
3.5.1 Configuring Persistence Providers (Entity Framework Core, MongoDB)
Elsa abstracts persistence, so you can plug in different providers. Here’s how to use Entity Framework Core with SQL Server (which we did earlier), or MongoDB.
Using MongoDB
First, add the package:
dotnet add package Elsa.Persistence.MongoDb
Then update your Elsa registration:
builder.Services.AddElsa(elsa => elsa
.UseMongoDbPersistence(builder.Configuration.GetConnectionString("ElsaMongo"))
.AddWorkflowsFrom<Program>());
Elsa will now store workflow instances, bookmarks, and logs in MongoDB collections. Choose SQL or NoSQL depending on your reporting, scaling, and deployment needs.
3.6 Triggering and Interacting with Workflows via API Endpoints
One of Elsa’s greatest strengths is the ability to orchestrate workflows from your application’s endpoints and vice versa.
Receiving HTTP Requests
Use the ReceiveHttpRequest activity in a workflow to bind a REST endpoint to workflow execution. As shown above, you can have Elsa handle requests directly, passing control to a workflow.
Starting and Managing Workflows from Code
Suppose you want to start a workflow when a document is uploaded:
[ApiController]
[Route("api/documents")]
public class DocumentsController : ControllerBase
{
private readonly IWorkflowRuntime _workflowRuntime;
public DocumentsController(IWorkflowRuntime workflowRuntime)
{
_workflowRuntime = workflowRuntime;
}
[HttpPost("submit")]
public async Task<IActionResult> Submit([FromBody] DocumentDto dto)
{
var workflowInstance = await _workflowRuntime.StartWorkflowAsync<DocumentApprovalWorkflow>(input: new { document = dto });
return Ok(new { workflowInstanceId = workflowInstance.Id });
}
}
You can also resume, suspend, cancel, or query workflows using Elsa’s runtime APIs.
Interacting with Human Tasks
For human steps, your frontend or workflow inbox UI can display pending approvals. On user action, you notify Elsa (typically via API) to continue the workflow from where it was blocked.
This approach enables you to tightly couple or decouple the workflow engine from your business logic and UI, depending on requirements.
4 Creating a Visual Modeler with bpmn.io
4.1 The Power of a Visual, Web-Based Modeler
While code-first workflow definition provides precision, most business stakeholders are more comfortable with visual models. A visual designer:
- Fosters business-IT collaboration.
- Enables process discovery and iterative refinement.
- Ensures BPMN standards compliance.
- Lowers the barrier to workflow authoring.
bpmn.io (the BPMN.js toolkit) is the de facto open-source BPMN editor. It provides:
- Intuitive drag-and-drop BPMN 2.0 modeling.
- Export/import of BPMN XML.
- Embedding in web applications.
- Customization and extensibility.
Integrating bpmn.io with your .NET backend allows you to round-trip between diagrams and executable workflows.
4.2 Integrating bpmn.io into an ASP.NET Core Application
There are two architectural models for using bpmn.io with .NET:
- Static Integration: Host the BPMN modeler as a static SPA (React, Angular, Blazor WASM, etc.) alongside your .NET backend.
- Server-Side Rendering: Embed BPMN.js in Razor pages or Blazor Server for tighter integration (less common due to JS interop complexity).
Example: Hosting bpmn.io in a React App with ASP.NET Core Backend
-
Create a React app (with Create React App or Vite):
npx create-react-app bpmn-modeler-ui -
Install bpmn.io libraries:
npm install bpmn-js -
Add the modeler to a component:
import BpmnModeler from 'bpmn-js/lib/Modeler'; function BpmnEditor() { useEffect(() => { const modeler = new BpmnModeler({ container: '#canvas', width: '100%', height: '600px' }); modeler.createDiagram(); // Add logic to load/save BPMN XML }, []); return <div id="canvas" />; } -
Expose API endpoints from your ASP.NET Core app for BPMN XML:
[ApiController] [Route("api/bpmn")] public class BpmnController : ControllerBase { private readonly IFileProvider _fileProvider; public BpmnController(IFileProvider fileProvider) { _fileProvider = fileProvider; } [HttpGet("{id}")] public IActionResult Get(string id) { var xml = _fileProvider.ReadAllText($"bpmn/{id}.bpmn"); return Content(xml, "application/xml"); } [HttpPost("{id}")] public async Task<IActionResult> Save(string id, [FromBody] string bpmnXml) { await _fileProvider.WriteAllTextAsync($"bpmn/{id}.bpmn", bpmnXml); return Ok(); } } -
Wire up the frontend to call these APIs to save/load diagrams.
This setup decouples the BPMN modeler UI from the .NET workflow engine, giving you flexibility to evolve each independently.
4.3 Saving and Loading BPMN Diagrams to/from Your .NET Backend
You have a few choices for persisting BPMN diagrams:
- Database: Store BPMN XML in a database table (recommended for versioning and audit).
- File System: For simple scenarios, save XML files to disk or cloud storage (Azure Blob, AWS S3).
- Document Storage: Use a document database like MongoDB.
A typical entity might look like this:
public class BpmnDiagram
{
public Guid Id { get; set; }
public string Name { get; set; }
public string BpmnXml { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
Your API would expose endpoints to create, read, update, and delete diagrams. Implement optimistic concurrency for collaborative editing.
4.4 Linking the Visual Model to Your Executable Workflow Definitions
How do you make a BPMN diagram actually drive code? This is where your investment in BPMN pays off.
Parsing BPMN XML and Executing as a Workflow
Elsa 3 has partial BPMN 2.0 support as of 2025. While not every BPMN construct is mapped 1:1, core elements—tasks, gateways, events—can be parsed and executed. The process typically involves:
- Import BPMN XML: Load the diagram created in bpmn.io into your backend.
- Transform to Elsa Workflow: Use Elsa’s BPMN parser to convert the BPMN XML into a workflow definition.
- Augment or Map Tasks: Attach custom code or activities to specific BPMN steps using extension points or conventions.
- Deploy and Execute: Persist the new workflow, which can now be started and interacted with like any other Elsa workflow.
Here’s a conceptual example:
// Load BPMN XML (from file, DB, or API)
var bpmnXml = File.ReadAllText("document-approval.bpmn");
// Parse BPMN to Elsa workflow definition
var parser = new BpmnParser();
var workflowDefinition = parser.Parse(bpmnXml);
// Register workflow definition for execution
workflowRegistry.Add(workflowDefinition);
Elsa’s BPMN support is evolving. For highly custom activities, you may need to supplement auto-generated workflows with additional code, or use custom activity types mapped to your BPMN tasks via task IDs or extension elements.
Keeping Models in Sync
For organizations practicing “model-driven development,” it’s important to:
- Establish conventions for mapping BPMN task IDs/names to activity classes or service calls.
- Implement round-tripping: changes in diagrams should reflect in workflows, and vice versa.
- Use test workflows to verify model-to-execution fidelity.
This approach creates a living artifact—the BPMN diagram—that both business and engineering can reason about, test, and evolve together.
5 From Diagram to Execution: A Practical Implementation
Modern workflow automation succeeds when modeling and execution stay tightly coupled. In this section, you’ll see how to bridge the gap between a visually-designed BPMN diagram (using bpmn.io) and executable logic (using Elsa 3), handling both technical and human workflow steps. By focusing on a Document Approval scenario, we’ll translate process intent into robust, maintainable code that runs in production.
5.1 Designing the Document Approval Process in bpmn.io
Begin by opening bpmn.io’s online modeler or your custom-integrated version.
Design the approval process as follows:
-
Start Event: “Document Submitted”
-
Service Task: “Compliance Check”
-
Exclusive Gateway: “Compliant?”
- Yes: Proceed to “Manager Approval” (User Task)
- No: End Event “Rejected: Non-Compliant”
-
User Task: “Manager Approval”
-
Exclusive Gateway: “Approved?”
- Yes: End Event “Approved”
- No: End Event “Rejected: Not Approved”
Add sequence flows and labels for clarity. Use Task IDs or Names that map cleanly to your code (e.g., ComplianceCheck, ManagerApproval). Save/export the diagram as BPMN XML (e.g., DocumentApproval.bpmn).
This visual process becomes your single source of truth, readable by both business and engineering. It’s a living contract—when business requirements change, you update the model first.
5.2 Implementing the BPMN Tasks as Elsa Activities
Parsing BPMN XML
Elsa 3’s BPMN support allows you to import a diagram and map each BPMN task to custom activity code. You can do this at app startup, via an admin portal, or dynamically as part of CI/CD.
Example: Loading and Registering the BPMN Workflow
var bpmnXml = System.IO.File.ReadAllText("DocumentApproval.bpmn");
var parser = new Elsa.Bpmn.Parser.BpmnParser();
var workflowDefinition = parser.Parse(bpmnXml);
// Register workflow for execution
workflowRegistry.Add(workflowDefinition);
If you need to extend BPMN parsing (e.g., to bind custom code to specific Service/User tasks), Elsa allows you to plug into the parsing pipeline or post-process the workflow definition.
5.2.1 User Tasks: Integrating with Human Actors
User Tasks represent manual steps—review, approve, or input. In Elsa, these map to blocking activities: the workflow pauses and waits for human input.
Implementing a User Task (e.g., “Manager Approval”)
Suppose you have a custom activity:
public class ManagerApprovalTask : Activity
{
[ActivityInput] public string DocumentId { get; set; }
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
// Create an approval record, notify the manager
await _taskService.CreateApprovalTaskAsync(DocumentId, context.WorkflowInstanceId);
// Block the workflow, awaiting a signal
context.WorkflowExecutionContext.Block("ManagerApproval", DocumentId);
}
}
When the manager takes action (approve/reject), your application signals the workflow to continue:
await workflowRuntime.ResumeWorkflowAsync(workflowInstanceId, "ManagerApproval", documentId, new Variables { ["isApproved"] = true });
Your BPMN model and code must share a contract (e.g., “ManagerApproval” as a signal/bookmark name) for the workflow to resume at the right step.
5.2.2 Service Tasks: Calling External APIs and Services
Service Tasks are automated—API calls, database updates, integrations.
Implementing a Service Task (e.g., “Compliance Check”)
You may write a custom activity:
public class ComplianceCheckTask : Activity
{
[ActivityInput] public string DocumentId { get; set; }
[ActivityOutput] public bool IsCompliant { get; set; }
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var complianceResult = await _complianceService.CheckAsync(DocumentId);
IsCompliant = complianceResult.Passed;
context.SetVariable("compliancePassed", IsCompliant);
}
}
You can then map the BPMN Service Task (with ID or name “ComplianceCheck”) to this activity when assembling the workflow definition. Elsa allows task-type mapping via extensions or via conventions (e.g., BPMN Task Name == Activity Type).
Managing Inputs and Outputs
All inputs/outputs (variables like isApproved, isCompliant) must be declared and handled consistently. Use Elsa’s workflow context or explicit mapping to pass data between activities.
5.3 Implementing BPMN Gateways for Conditional Logic
BPMN Gateways (especially Exclusive Gateways) control branching.
Example: Modeling Conditional Approval Flow
Your BPMN may include a gateway with condition expressions (e.g., ${compliancePassed} or ${isApproved}). Elsa’s workflow definition will implement these using IfElse or equivalent logic:
.Then<IfElse>(setup =>
{
setup.Condition = ctx => ctx.GetVariable<bool>("compliancePassed");
setup.WhenTrue = branch => branch.Then<ManagerApprovalTask>();
setup.WhenFalse = branch => branch.Then<SendRejectionNotificationTask>();
});
If you’re parsing BPMN XML, Elsa’s parser can translate BPMN condition expressions into C# expressions or delegate them to a custom evaluator.
Tips for Reliable Gateway Logic:
- Keep condition logic simple and transparent (avoid side effects).
- Clearly document variable names and flow outcomes in both the diagram and code.
- Test each branch thoroughly (see Testing section below).
5.4 Running the Workflow: A Step-by-Step Walkthrough
Let’s walk through the end-to-end process:
1. Submission
A user POSTs a document via API. This triggers workflow instantiation:
await _workflowRuntime.StartWorkflowAsync<DocumentApprovalWorkflow>(input: new { documentId = "123", submitterEmail = "user@company.com" });
2. Compliance Check
The ComplianceCheckTask is executed—calling external services, saving results in workflow state.
3. Gateway: Compliant?
- If
compliancePassedistrue, the workflow continues to the Manager Approval User Task. - If
false, the workflow sends a rejection notification and ends.
4. Manager Approval
The workflow pauses, creating a pending task for the manager (via a UI, notification, etc.). The manager logs in, reviews, and clicks Approve or Reject. This signals the workflow to continue:
await _workflowRuntime.ResumeWorkflowAsync(instanceId, "ManagerApproval", documentId, new Variables { ["isApproved"] = true });
5. Gateway: Approved?
- If
isApprovedistrue, send an approval notification and complete the workflow. - If
false, send a rejection notification and complete the workflow.
6. Audit and Observability
Throughout, all steps, data changes, and user actions are logged and can be monitored (see Monitoring section).
6 Advanced Architectural Considerations
Building and running production-grade workflow automation isn’t just about happy paths. Real-world workflows face failure, evolution, security demands, and operational complexity. This section covers what senior architects need to master for resilient, auditable, and maintainable workflow systems.
6.1 Error Handling and Compensation in Workflows
Workflows span boundaries: databases, APIs, human actors. Failures are inevitable—network outages, validation errors, declined approvals, or business rule violations.
Error Handling Approaches
- Activity-Level Try/Catch: Elsa supports catching exceptions at the activity level, with error boundary events or by using explicit error handling constructs.
- Global Error Policies: Define default actions for unhandled exceptions (log, retry, escalate, abort, or trigger compensating flows).
- Custom Error Activities: Use custom activities to interpret error codes or business-specific exceptions (e.g., “OnValidationFailure”, “OnApiTimeout”).
Compensation Logic
BPMN’s compensation events model undo logic (e.g., rollback a booking if payment fails). Elsa lets you implement compensating activities explicitly, though native BPMN compensation is evolving.
Example: Compensation in Elsa
Suppose a workflow reserves inventory, then books shipment. If shipment fails, you want to release inventory:
.Then<ReserveInventoryTask>()
.Then<TryCatch>(tryCatch =>
{
tryCatch.Try = tryBranch => tryBranch.Then<BookShipmentTask>();
tryCatch.Catch = catchBranch => catchBranch.Then<ReleaseInventoryTask>();
});
Best Practices:
- Make compensation idempotent (safe to call multiple times).
- Document which steps require compensation and test failure scenarios.
- Integrate alerting for repeated or critical compensations.
6.2 Versioning and Migrating Long-Running Workflow Instances
Workflows often run for days or weeks. What happens if you must change the workflow logic mid-flight?
Versioning Strategies
- Immutable Workflow Definitions: Once a workflow instance starts, it continues using the definition version it started with.
- Versioned Definitions: Elsa supports registering multiple versions; new instances use the latest, but running instances finish on their original.
- Migrations: If a process change is critical (e.g., compliance fix), you can migrate instances by mapping old states to new definitions. This is advanced and requires explicit handling—such as custom migration scripts.
Practical Tips
- Design workflows with upgradeability in mind—avoid tightly coupling state to activity implementation details.
- Use explicit state versioning and migration plans for regulated or business-critical processes.
- Log and audit which version each workflow instance used for traceability.
6.3 Securing Workflows: Authentication and Authorization
Security is critical. Workflow engines touch sensitive data, orchestrate business logic, and trigger actions on behalf of users.
Authentication
- Protect workflow APIs with standard authentication (JWT, OAuth2, cookie auth, etc.).
- Ensure that only authenticated actors can trigger, resume, or manage workflows.
Authorization
- Implement fine-grained authorization at activity level (e.g., only managers can approve certain tasks).
- Use claims-based or role-based access controls (RBAC).
- Filter user task inboxes based on actor identity and permissions.
Example: Restricting Task Access
Suppose a workflow pauses for “ManagerApproval.” Only users with the Manager role should see or interact with this task:
if (!user.IsInRole("Manager"))
return Forbid();
- Use Elsa’s activity context and your application’s security context to enforce rules.
- Record all access attempts and decisions for auditing.
Data Protection
- Secure workflow state at rest (database encryption, secure backups).
- Avoid storing sensitive data unnecessarily in workflow variables—reference IDs instead of full PII where possible.
6.4 Testing Your Workflow Logic: Unit and Integration Testing Strategies
Testing workflow-driven systems is more involved than testing stateless code. You must validate both process logic and side effects across activities, services, and user tasks.
Unit Testing Activities
- Test custom activities in isolation (given input variables, assert expected output and state).
- Mock external dependencies (APIs, databases, notifications).
Example: Testing a Custom Activity
[Fact]
public async Task ComplianceCheckTask_ReturnsCorrectResult()
{
var activity = new ComplianceCheckTask();
var context = new TestActivityExecutionContext { /* Setup mocks and inputs */ };
await activity.ExecuteAsync(context);
Assert.True(context.GetVariable<bool>("compliancePassed"));
}
Integration Testing Workflows
- Use Elsa’s in-memory runtime for running workflows end-to-end in test environments.
- Trigger workflows via test APIs, simulate human interactions, and assert outcomes.
Example: Simulating Human Approval
// Start workflow and capture instance ID
var instanceId = await _workflowRuntime.StartWorkflowAsync<DocumentApprovalWorkflow>(input);
// Simulate manager approval
await _workflowRuntime.ResumeWorkflowAsync(instanceId, "ManagerApproval", documentId, new Variables { ["isApproved"] = true });
// Assert final workflow state
var workflowInstance = await _workflowStore.FindByIdAsync(instanceId);
Assert.Equal(WorkflowStatus.Finished, workflowInstance.Status);
Test Data Management
- Use transactional test databases or reset state between runs.
- Prefer synthetic (non-PII) data for integration tests.
Automated Regression Testing
- Version and test all workflow definitions before deploying changes.
- Use test coverage to identify unused or risky branches.
6.5 Monitoring and Auditing Workflow Execution
Workflow engines automate core business processes—observability isn’t optional.
Monitoring
- Execution Logs: Track workflow instance status, activity execution, inputs/outputs, errors.
- Dashboards: Elsa provides a built-in dashboard; integrate with your monitoring stack (Application Insights, Prometheus, Grafana).
- Alerts: Set up alerts for failed, suspended, or overdue workflows.
Example: Emitting Custom Metrics
Integrate Elsa with a metrics provider:
services.AddElsa(elsa => elsa
.AddWorkflowObserver<MyCustomObserver>());
Where MyCustomObserver pushes custom events/metrics.
Auditing
- User Actions: Log every human decision (who approved/rejected, when, comments).
- Data Changes: Record key variable changes for sensitive processes.
- Compliance: Retain audit trails for regulatory purposes.
Accessing Workflow History
Elsa supports querying execution history via API or dashboard. You can build custom UIs for compliance, reporting, or operational troubleshooting.
Long-Term Retention
- Implement log rotation and archival for large-scale environments.
- Anonymize or purge sensitive records as required by policy.
7 Integrating with the Broader .NET Ecosystem
A workflow engine never operates in isolation. Real-world business solutions demand seamless integration with the surrounding application landscape. For .NET architects, this means ensuring workflows are both loosely coupled and responsive, while also being easy to deploy, scale, and observe. This section explores three pillars of integration: MediatR for decoupled messaging, SignalR for real-time feedback, and Docker for deployment consistency.
7.1 Leveraging MediatR for Decoupled Communication
One of the enduring challenges in workflow-centric architectures is managing cross-cutting concerns—notifications, data persistence, event sourcing—without creating brittle dependencies between your workflow logic and the rest of your application.
MediatR is a popular in-process messaging library for .NET. It enables the publish/subscribe and request/response patterns, fostering clean, maintainable code with minimal coupling.
Why Use MediatR with Elsa Workflows?
- Separation of Concerns: Keep business logic isolated from infrastructure (email, logging, auditing).
- Extensibility: Add or change side effects (notifications, logging) without editing workflow code.
- Testability: Easily mock, intercept, or verify events in unit/integration tests.
Practical Example: Publishing Workflow Events
Suppose your document approval workflow needs to notify multiple services when a document is approved—updating a CRM, sending emails, logging an audit entry.
Step 1: Define a Notification
public class DocumentApprovedNotification : INotification
{
public string DocumentId { get; }
public string ApprovedBy { get; }
public DocumentApprovedNotification(string documentId, string approvedBy)
{
DocumentId = documentId;
ApprovedBy = approvedBy;
}
}
Step 2: Publish from a Workflow Activity
Within a custom Elsa activity:
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var mediator = context.GetRequiredService<IMediator>();
var documentId = context.GetVariable<string>("documentId");
var approvedBy = context.GetVariable<string>("approvedBy");
await mediator.Publish(new DocumentApprovedNotification(documentId, approvedBy));
}
Step 3: Handle Notifications Elsewhere
Define one or more handlers:
public class SendApprovalEmailHandler : INotificationHandler<DocumentApprovedNotification>
{
public async Task Handle(DocumentApprovedNotification notification, CancellationToken cancellationToken)
{
// Send an email or message.
}
}
public class LogApprovalHandler : INotificationHandler<DocumentApprovedNotification>
{
public Task Handle(DocumentApprovedNotification notification, CancellationToken cancellationToken)
{
// Log to audit trail.
return Task.CompletedTask;
}
}
This approach means your workflow code remains focused—side effects are managed elsewhere, easy to update or extend. Over time, this is essential for maintainability as requirements grow.
7.2 Using SignalR for Real-Time Workflow Updates
Workflows often involve waiting for human actions or asynchronous events. Users (and business systems) expect timely feedback, especially for approvals, escalations, or timeouts.
SignalR is Microsoft’s library for real-time web functionality—pushing data instantly from server to clients. With SignalR, you can update dashboards, notify users, and enable collaborative workflow scenarios.
Common Real-Time Scenarios
- Approval Inbox: Users see tasks assigned to them appear instantly as the workflow engine creates them.
- Process Monitoring: Operations staff track workflow progress, errors, or escalations in real time.
- Collaborative Review: Multiple users see state changes to the same process, reducing duplicate work.
Integrating Elsa and SignalR
1. Define SignalR Hub
public class WorkflowHub : Hub
{
// Define methods for client notifications, e.g., TaskAssigned, WorkflowCompleted.
}
2. Notify Clients from Activities or Workflow Events
In a custom activity:
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var hubContext = context.GetRequiredService<IHubContext<WorkflowHub>>();
var userId = context.GetVariable<string>("assignedUserId");
await hubContext.Clients.User(userId)
.SendAsync("TaskAssigned", new { documentId = ... });
}
3. Subscribe on the Frontend
Clients (web, desktop, mobile) connect via SignalR and handle notifications:
const connection = new signalR.HubConnectionBuilder()
.withUrl("/workflowHub")
.build();
connection.on("TaskAssigned", task => {
// Update UI with new approval task.
});
This pattern delivers a modern, responsive user experience and reduces polling or manual refresh cycles. It also lays the groundwork for richer process collaboration tools.
7.3 Containerizing Your Workflow Engine with Docker
As your workflow solution matures, deployment consistency, portability, and scalability become central. Containerization—using Docker—enables you to package your .NET application, including Elsa and all its dependencies, into a standard, reproducible unit.
Why Containerize?
- Consistency: Eliminates “works on my machine” issues.
- Scalability: Easily scale out workflow processing via orchestrators (Kubernetes, Azure Container Apps, AWS ECS).
- DevOps Efficiency: Streamlines CI/CD, blue-green deployments, and rollback.
Creating a Dockerfile for Your Elsa Workflow App
Here’s a basic Dockerfile for an ASP.NET Core + Elsa workflow engine:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["DocumentApprovalDemo/DocumentApprovalDemo.csproj", "DocumentApprovalDemo/"]
RUN dotnet restore "DocumentApprovalDemo/DocumentApprovalDemo.csproj"
COPY . .
WORKDIR "/src/DocumentApprovalDemo"
RUN dotnet build "DocumentApprovalDemo.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "DocumentApprovalDemo.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "DocumentApprovalDemo.dll"]
Build and run:
docker build -t document-approval-demo .
docker run -d -p 8080:80 --name doc-approval document-approval-demo
Persisting Workflow State
- Use external databases (SQL Server, PostgreSQL, MongoDB) in containers or managed services.
- Store logs, attachments, or artifacts in persistent volumes or cloud storage.
Scaling and Orchestration
- Use Kubernetes, Azure Container Apps, or AWS ECS to manage replicas, health, rolling updates, and failover.
- Monitor health endpoints and logs for auto-scaling and operational transparency.
This approach sets the stage for robust, enterprise-grade workflow automation—fully leveraging cloud-native and DevOps capabilities.
8 The Future of Workflow Automation in .NET
Workflow automation is entering a new era. Driven by business agility demands and powered by rapid advances in developer tooling, AI, and low-code platforms, architects must balance innovation with the enduring strengths of code-first solutions.
8.1 The Rise of Low-Code and No-Code Platforms
Low-code and no-code platforms have moved from trend to mainstream. Vendors like Microsoft (Power Automate), Camunda, and ServiceNow enable business users to automate processes with drag-and-drop designers and pre-built connectors—often with minimal IT involvement.
What’s Driving Adoption?
- Speed: Rapidly prototype, iterate, and deploy solutions.
- Empowerment: Enable non-developers to build, adapt, and maintain automations.
- Integration: Seamlessly connect SaaS, legacy, and custom systems.
Implications for .NET Architects
- Guardrails Needed: Low-code platforms can introduce technical debt if not governed. Architects must set patterns for data, integration, and security.
- Hybrid Approaches: Combine low-code for simple processes, code-first for complex, mission-critical workflows.
- Extensibility: Most platforms provide APIs, webhooks, or plugin models for deep .NET integration.
Takeaway: Low-code increases reach and velocity, but strategic processes still demand code-first rigor and oversight.
8.2 The Impact of AI and Machine Learning on Business Processes
AI and machine learning are redefining process automation:
- Intelligent Routing: Use ML models to assign tasks based on context, past outcomes, or workload balancing.
- Document Understanding: Automate data extraction and validation using AI-powered OCR and NLP.
- Predictive Process Mining: Analyze workflow execution logs to detect bottlenecks and suggest optimizations.
- Conversational Automation: Integrate with chatbots and virtual assistants to enable conversational approvals and escalations.
How to Leverage AI in .NET Workflows
- Integrate with Azure Cognitive Services, OpenAI, or custom ML models via Elsa Service Tasks.
- Use AI to enrich decision points—e.g., flagging anomalous requests for manual review.
- Monitor and audit AI-driven decisions for transparency and compliance.
Caution: AI augments, but does not replace, sound process design. Maintain human oversight and clear escalation paths.
8.3 The Enduring Value of a Code-First, BPMN-Driven Approach
Despite the rise of low-code and AI, a code-first, BPMN-driven architecture remains vital for many organizations:
- Traceability: Every step is visible, versioned, and auditable—from model to execution.
- Control: Custom logic, integrations, and exception handling are easy to extend and maintain.
- Collaboration: BPMN bridges business and IT, enabling iterative improvement without losing intent in translation.
- Adaptability: Processes evolve safely—code-first workflows can be tested, versioned, and migrated as business needs change.
Even as platforms become more sophisticated, the foundational patterns you’ve learned—clear modeling, robust code, modular design, and operational excellence—endure.
9 Conclusion: Empowering Your Organization with Custom Workflows
As you reach the conclusion of this deep-dive, you are equipped with not just technical recipes, but a strategic playbook for architecting and delivering business process automation in the .NET world.
9.1 Key Takeaways for Architects
- Start with the Business: Anchor every workflow initiative in clear, validated business outcomes.
- Model First: Use BPMN as a living contract. Foster collaboration across business and technology teams.
- Choose the Right Engine: Elsa, Workflow Core, Camunda, or other—align your selection with your unique integration, scalability, and governance needs.
- Code with Care: Balance code-first power with maintainability. Invest in custom activities and clear contracts between model and implementation.
- Automate Governance: Version, test, and audit workflows as rigorously as core application code.
- Integrate Broadly: Leverage the .NET ecosystem—MediatR, SignalR, Docker, and cloud services—for a responsive, scalable, and maintainable solution.
- Prepare for Change: Design for process evolution. Embrace low-code where it fits, but retain code-first discipline for mission-critical logic.
- Watch the Horizon: AI and process mining are changing what’s possible. Stay engaged with the community and emerging practices.
9.2 Final Recommendations and Best Practices
- Pilot Before Scaling: Start with a single, high-impact process to validate your architecture and team practices.
- Invest in Training: Upskill both developers and analysts in BPMN, workflow tooling, and best practices.
- Automate Testing: Build a CI/CD pipeline for workflow definitions, tests, and deployment.
- Prioritize Observability: Instrument every workflow. Build dashboards and alerts for real-world reliability.
- Document Everything: Maintain up-to-date diagrams, workflow code, and operational runbooks.
- Iterate with Feedback: Use metrics, user input, and business outcomes to refine and extend your workflow portfolio.
- Promote Ownership: Assign clear process and technical owners for each workflow; build a culture of shared responsibility.
9.3 Further Reading and Community Resources
- Elsa Workflows Documentation: https://v2.elsaworkflows.io/
- bpmn.io Documentation and Demos: https://bpmn.io/
- .NET Foundation: https://dotnetfoundation.org/
- BPMN 2.0 Specification (OMG): https://www.omg.org/spec/BPMN/2.0/
- MediatR for .NET: https://github.com/jbogard/MediatR
- SignalR for ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction
- Microsoft’s Modern Architecture Guidance: https://learn.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/
- Elsa Community Discussions: https://github.com/elsa-workflows/elsa-core/discussions
- Process Mining and Workflow Trends: https://www.celonis.com/